From ab41bc24eced090c06b19a4b19796b44652d12ae Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Mon, 11 May 2026 08:02:44 +0100 Subject: [PATCH] feat: auto-generate CI jobs from flake checks Add `checks` option and `config.autoChecks` to auto-generate a CI job for each flake check derivation. Auto-generated jobs go through `mkJobPatched` for proper Nix setup, cache, and environment handling. The `checks` option is automatically wired to `config.checks` in flake-parts. System is inferred from `pkgs.system` and set via the new `system` option. User-defined jobs with the same name always take precedence. New tests cover both flake-parts and direct API entry points. MT-14138 --- docs/usage.md | 80 +++++++++++++ lib/default.nix | 2 +- lib/flakeModule.nix | 8 +- lib/impl/modules/pipeline.nix | 26 ++++- lib/impl/modules/root.nix | 73 +++++++++++- tests/auto_checks_test.nix | 65 +++++++++++ .../fixtures/flake_parts_autochecks/flake.nix | 33 ++++++ tests/modules_test.nix | 106 ++++++++++++++++++ 8 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 tests/auto_checks_test.nix create mode 100644 tests/fixtures/flake_parts_autochecks/flake.nix diff --git a/docs/usage.md b/docs/usage.md index 44c0bf7..5619c0d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -67,6 +67,86 @@ in # exposes `soonix` for the soonix hook and `packages` which contain the configs, jobs etc. ``` +## Auto-generate jobs from flake checks + +You can auto-generate CI jobs from your flake checks with the `autoChecks` option. +Each check becomes a job that runs `nix build .#checks..`. +Auto-generated jobs are skipped if a user-defined job with the same name exists. + +### With flake-parts + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default-linux"; + nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci?dir=lib"; + }; + + outputs = + { flake-parts, systems, ... } @ inputs: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.nix-gitlab-ci.flakeModule ]; + systems = import systems; + perSystem = { pkgs, system, ... }: { + checks = { + lint = pkgs.runCommand "lint" { } '' + ${pkgs.nodePackages.prettier}/bin/prettier --check . + touch $out + ''; + test = pkgs.runCommand "test" { } '' + ${pkgs.python3}/bin/python -m pytest + touch $out + ''; + }; + ci = { + config.autoChecks.enable = true; + pipelines.default.stages = [ "test" "build" ]; + }; + }; + }; +} +``` + +### Without flake-parts + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci?dir=lib"; + }; + + outputs = + { self, nixpkgs, nix-gitlab-ci, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + cilib = nix-gitlab-ci.lib { inherit pkgs; }; + in + { + checks.${system} = { + lint = pkgs.runCommand "lint" { } '' + ${pkgs.nodePackages.prettier}/bin/prettier --check . + touch $out + ''; + test = pkgs.runCommand "test" { } '' + ${pkgs.python3}/bin/python -m pytest + touch $out + ''; + }; + + packages.${system} = + (cilib.mkCI { + checks = self.checks.${system}; + config.autoChecks.enable = true; + pipelines.default.stages = [ "test" "build" ]; + }).packages; + }; +} +``` + ______________________________________________________________________ Since V2 multiple pipelines are supported. diff --git a/lib/default.nix b/lib/default.nix index ff3ec88..02b407d 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -20,7 +20,7 @@ args: let modules = [ cilib.modules.nixCiSubmodule { - inherit config; + config = config // {inherit (pkgs) system;}; } ]; }).config; diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index aadf292..1af34c6 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -18,7 +18,13 @@ default = {}; }; }; - config.legacyPackages = config.ci.packages; + config = { + ci = { + inherit (config) checks; + inherit (pkgs) system; + }; + legacyPackages = config.ci.packages; + }; } ); } diff --git a/lib/impl/modules/pipeline.nix b/lib/impl/modules/pipeline.nix index 32b9e0c..0139b3d 100644 --- a/lib/impl/modules/pipeline.nix +++ b/lib/impl/modules/pipeline.nix @@ -24,6 +24,9 @@ name, config, rootConfig, + rootChecks ? {}, + rootAutoChecks ? {}, + system, ... }: let # @@ -211,10 +214,31 @@ // gitlabOptions; config = let attrsToKeep = builtins.attrNames gitlabOptions; + autoNixConfig = { + enable = config.nix.nixJobsByDefault; + enableRunnerCache = false; + }; + autoJobs = lib.optionalAttrs (rootAutoChecks.enable && name == rootAutoChecks.pipeline) ( + builtins.mapAttrs (checkName: _: + cilib.mkJobPatched { + key = "auto:${checkName}"; + pipelineName = name; + nixConfig = autoNixConfig; + job = { + image = "$NIX_CI_IMAGE"; + stage = rootAutoChecks.stage; + script = ["nix build .#checks.\"${system}\".${checkName}"]; + } // lib.optionalAttrs (rootAutoChecks.tags != []) {tags = rootAutoChecks.tags;}; + } + ) ( + builtins.removeAttrs rootChecks (builtins.attrNames config.jobs) + ) + ); in { finalConfig = (filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config)) - // mapAttrs (_name: value: value.finalConfig) config.jobs; + // mapAttrs (_name: value: value.finalConfig) config.jobs + // autoJobs; packages = { "gitlab-ci:pipeline:${name}" = toYaml "gitlab-ci-config.json" config.finalConfig; diff --git a/lib/impl/modules/root.nix b/lib/impl/modules/root.nix index bd99050..ab464fc 100644 --- a/lib/impl/modules/root.nix +++ b/lib/impl/modules/root.nix @@ -6,6 +6,41 @@ }: let inherit (lib) mkOption types foldr; in rec { + autoChecksSubmodule = { + options = { + enable = mkOption { + description = '' + Auto-generate CI jobs from [`checks`](#checks). + + Each check becomes a job that runs `nix build .#checks..`. + ''; + type = types.bool; + default = false; + }; + stage = mkOption { + description = '' + Stage to assign to auto-generated check jobs. + ''; + type = types.str; + default = "test"; + }; + tags = mkOption { + description = '' + Tags to assign to auto-generated check jobs. + ''; + type = types.listOf types.str; + default = []; + }; + pipeline = mkOption { + description = '' + Name of the pipeline to add auto-generated check jobs to. + ''; + type = types.str; + default = "default"; + }; + }; + }; + configSubmodule = { options = { soonix = mkOption { @@ -24,11 +59,40 @@ in rec { type = types.bool; default = true; }; + autoChecks = mkOption { + description = '' + Auto-generate CI jobs from flake checks. + + See [`config.autoChecks`](#configautochecks) for configuration options. + ''; + type = types.submodule autoChecksSubmodule; + default = {}; + }; }; }; - nixCiSubmodule = {config, ...}: { + nixCiSubmodule = {config, ...}: let + system = config.system; + in { options = { + system = mkOption { + description = '' + System identifier (e.g. `"x86_64-linux"`). + Used when building checks with `nix build .#checks..`. + Set automatically by `flakeModule` or `cilib.mkCI`. + ''; + type = types.str; + }; + checks = mkOption { + description = '' + Nix flake checks to optionally auto-generate CI jobs from. + + Each attribute must be a derivation. The attribute name becomes the CI job name. + Requires [`config.autoChecks.enable`](#configautochecksenable) to take effect. + ''; + type = types.attrsOf types.package; + default = {}; + }; config = mkOption { description = '' Configuration of Nix-GitLab-CI itself. @@ -42,7 +106,12 @@ in rec { ''; type = types.attrsOf (types.submoduleWith { modules = [pipelineSubmodule]; - specialArgs.rootConfig = config.config; + specialArgs = { + rootConfig = config.config; + rootChecks = config.checks; + rootAutoChecks = config.config.autoChecks; + inherit system; + }; }); default = {}; }; diff --git a/tests/auto_checks_test.nix b/tests/auto_checks_test.nix new file mode 100644 index 0000000..69c9b6e --- /dev/null +++ b/tests/auto_checks_test.nix @@ -0,0 +1,65 @@ +{ + pkgs, + ntlib, + cilib, + ... +}: { + suites."autoChecks" = { + pos = __curPos; + tests = [ + { + name = "flakeModule"; + type = "script"; + script = + # sh + '' + ${ntlib.helpers.scriptHelpers} + ${ntlib.helpers.path (with pkgs; [coreutils nix gnused gnugrep jq])} + export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + repo_path=${../.} + + cp ${./fixtures/flake_parts_autochecks}/* . + sed -i -e "s|@repo_path@|$repo_path|" flake.nix + + nix build --impure .#gitlab-ci:pipeline:default + assert "-f result" "should exist" + + system=$(nix eval --impure --expr "builtins.currentSystem" --raw) + jq -e '."my-check" | .stage == "test"' result > /dev/null + jq -e '."my-check" | .script[0] | contains("nix build")' result > /dev/null + jq -e '."my-check" | .script[0] | contains("'"$system"'")' result > /dev/null + jq -e '."my-check" | has("before_script")' result > /dev/null + jq -e '."my-check" | has("after_script")' result > /dev/null + ''; + } + { + name = "direct"; + type = "script"; + script = let + system = pkgs.system; + fakeCheck = pkgs.runCommand "fake-check" {} "touch $out"; + ci = cilib.mkCI { + checks.my-check = fakeCheck; + config.autoChecks.enable = true; + pipelines.default.stages = [ "test" ]; + }; + pkg = ci.packages."gitlab-ci:pipeline:default"; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + ${ntlib.helpers.path (with pkgs; [coreutils gnugrep jq])} + + json=$(cat ${pkg}) + echo "$json" | jq -e '.stages == [".pre", "test", ".post"]' > /dev/null + echo "$json" | jq -e '."my-check" | .stage == "test"' > /dev/null + echo "$json" | jq -e '."my-check" | .script[0] | contains("nix build")' > /dev/null + echo "$json" | jq -e '."my-check" | .script[0] | contains("${system}")' > /dev/null + echo "$json" | jq -e '."my-check" | has("before_script")' > /dev/null + echo "$json" | jq -e '."my-check" | has("after_script")' > /dev/null + ''; + } + ]; + }; +} diff --git a/tests/fixtures/flake_parts_autochecks/flake.nix b/tests/fixtures/flake_parts_autochecks/flake.nix new file mode 100644 index 0000000..592bcc4 --- /dev/null +++ b/tests/fixtures/flake_parts_autochecks/flake.nix @@ -0,0 +1,33 @@ +{ + outputs = { + flake-parts, + systems, + ... + } @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + "@repo_path@/lib/flakeModule.nix" + ]; + systems = import systems; + flake = {}; + perSystem = {pkgs, ...}: let + fakeCheck = pkgs.runCommand "fake-check" {} "touch $out"; + in { + checks = { + my-check = fakeCheck; + }; + ci = { + config.autoChecks.enable = true; + pipelines.default = { + stages = ["test"]; + }; + }; + }; + }; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default-linux"; + }; +} diff --git a/tests/modules_test.nix b/tests/modules_test.nix index 8ccbd83..1e5840b 100644 --- a/tests/modules_test.nix +++ b/tests/modules_test.nix @@ -98,6 +98,112 @@ assert_file_contains ${package} 'export EXAMPLE="/nix/store/.*-hello-.*"' ''; } + { + name = "autoChecks with no checks set"; + expected = { + stages = [".pre" "test" ".post"]; + }; + actual = + (cilib.mkCI { + config.autoChecks.enable = true; + pipelines."default".stages = ["test"]; + }).pipelines."default".finalConfig; + } + { + name = "autoChecks generates check jobs"; + expected = { + stages = [".pre" "check" ".post"]; + unit = { + image = "$NIX_CI_IMAGE"; + stage = "test"; + script = ["nix build .#checks.\"${pkgs.system}\".unit"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:auto:unit\""]; + after_script = ["finalize_nix_ci"]; + }; + lint = { + image = "$NIX_CI_IMAGE"; + stage = "test"; + script = ["nix build .#checks.\"${pkgs.system}\".lint"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:auto:lint\""]; + after_script = ["finalize_nix_ci"]; + }; + }; + actual = let + fakeCheck = pkgs.runCommand "fake-check" {} "touch $out"; + in + (cilib.mkCI { + config.autoChecks = { + enable = true; + }; + checks = { + unit = fakeCheck; + lint = fakeCheck; + }; + pipelines."default".stages = ["check"]; + }).pipelines."default".finalConfig; + } + { + name = "autoChecks skips existing jobs"; + expected = { + stages = [".pre" "check" "build" ".post"]; + unit = { + image = "$NIX_CI_IMAGE"; + stage = "test"; + script = ["nix build .#checks.\"${pkgs.system}\".unit"]; + }; + custom = { + image = "$NIX_CI_IMAGE"; + stage = "build"; + script = ["echo custom"]; + }; + }; + actual = let + fakeCheck = pkgs.runCommand "fake-check" {} "touch $out"; + in + (cilib.mkCI { + config.autoChecks = { + enable = true; + }; + checks = { + unit = fakeCheck; + custom = fakeCheck; + }; + pipelines."default" = { + nix.nixJobsByDefault = false; + stages = ["check" "build"]; + jobs."custom" = { + stage = "build"; + script = ["echo custom"]; + }; + }; + }).pipelines."default".finalConfig; + } + { + name = "autoChecks uses custom stage and tags"; + expected = { + stages = [".pre" "quality" ".post"]; + lint = { + image = "$NIX_CI_IMAGE"; + stage = "quality"; + tags = ["docker" "nix"]; + script = ["nix build .#checks.\"${pkgs.system}\".lint"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:auto:lint\""]; + after_script = ["finalize_nix_ci"]; + }; + }; + actual = let + fakeCheck = pkgs.runCommand "fake-check" {} "touch $out"; + in + (cilib.mkCI { + config.autoChecks = { + enable = true; + stage = "quality"; + tags = ["docker" "nix"]; + }; + checks.lint = fakeCheck; + pipelines."default".stages = ["quality"]; + }).pipelines."default".finalConfig; + } ]; }; }