From 70dc87811240f34d24c45ab0465c52f8200cad10 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Wed, 13 May 2026 08:53:15 +0100 Subject: [PATCH] fix: pipelines can have either trigger or script They canno thave both, so here I add assertions support and use them to avoid this case. --- .gitignore | 1 + lib/default.nix | 22 +++++++++------ lib/impl/default.nix | 2 +- lib/impl/modules/default.nix | 7 +++-- lib/impl/modules/job.nix | 20 +++++++++++-- lib/impl/modules/pipeline.nix | 13 ++++++--- lib/impl/modules/root.nix | 8 +++++- tests/modules_test.nix | 53 +++++++++++++++++++++++++++++++++++ 8 files changed, 107 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index fe07db9..370b114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .ren/ +junit.xml result diff --git a/lib/default.nix b/lib/default.nix index ff3ec88..225ec95 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -16,14 +16,20 @@ args: let } (builtins.readFile ./VERSION); mkCI = config: - (evalModules { - modules = [ - cilib.modules.nixCiSubmodule - { - inherit config; - } - ]; - }).config; + let + result = (evalModules { + modules = [ + cilib.modules.nixCiSubmodule + { + inherit config; + } + ]; + }).config; + # Check all assertions and warnings using NixOS infrastructure, + # then return the result with internal module options removed + checked = lib.asserts.checkAssertWarn result.assertions result.warnings result; + in + builtins.removeAttrs checked ["assertions" "warnings" "config"]; }; in cilib diff --git a/lib/impl/default.nix b/lib/impl/default.nix index 7ff5623..a5bf363 100644 --- a/lib/impl/default.nix +++ b/lib/impl/default.nix @@ -8,5 +8,5 @@ mkJobRun = import ./jobRun.nix {inherit lib pkgs helpers;}; mkJobPatched = import ./jobPatched.nix {inherit lib helpers;}; mkPipeline = import ./pipeline.nix {inherit lib helpers mkJobDeps mkJobRun mkJobPatched;}; - modules = import ./modules {inherit lib cilib;}; + modules = import ./modules {inherit lib pkgs cilib;}; } diff --git a/lib/impl/modules/default.nix b/lib/impl/modules/default.nix index 9e33a7e..f32a45f 100644 --- a/lib/impl/modules/default.nix +++ b/lib/impl/modules/default.nix @@ -1,24 +1,25 @@ { lib, + pkgs, cilib, }: rec { inherit (import ./root.nix { - inherit lib pipelineSubmodule soonixSubmodule; + inherit lib pkgs pipelineSubmodule soonixSubmodule; }) configSubmodule nixCiSubmodule ; inherit (import ./pipeline.nix { - inherit lib cilib jobSubmodule; + inherit lib pkgs cilib jobSubmodule; }) pipelineConfigSubmodule pipelineSubmodule ; inherit (import ./job.nix { - inherit lib cilib; + inherit lib pkgs cilib; }) jobConfigSubmodule jobSubmodule diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index fd4fc62..8b7c973 100644 --- a/lib/impl/modules/job.nix +++ b/lib/impl/modules/job.nix @@ -1,10 +1,11 @@ { lib, cilib, + pkgs, ... }: let inherit (lib) mkOption types filterAttrs; - inherit (cilib.helpers) filterUnset mkUnsetOption eitherWithSubOptions; + inherit (cilib.helpers) filterUnset mkUnsetOption eitherWithSubOptions isUnset; in rec { jobConfigSubmodule = {pipelineConfig, ...}: { options = { @@ -511,11 +512,13 @@ in rec { [Docs](https://docs.gitlab.com/ci/yaml/#rules) ''; }; - script = mkOption { + script = mkUnsetOption { type = types.listOf types.str; description = '' Shell script that is executed by a runner. + A job can have either `script` or `trigger` defined, but not both. + [Docs](https://docs.gitlab.com/ci/yaml/#script) ''; }; @@ -567,6 +570,8 @@ in rec { description = '' Defines a downstream pipeline trigger. + A job can have either `script` or `trigger` defined, but not both. + [Docs](https://docs.gitlab.com/ci/yaml/#trigger) ''; }; @@ -589,6 +594,7 @@ in rec { }; }; in { + imports = [(pkgs.path + /nixos/modules/misc/assertions.nix)]; options = { nix = mkOption { @@ -636,9 +642,19 @@ in rec { } // gitlabOptions; config = let + + attrsToKeep = builtins.attrNames gitlabOptions; job = filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config); + jobHasScript = builtins.hasAttr "script" job; + jobHasTrigger = builtins.hasAttr "trigger" job; in { + assertions = [ + { + assertion = !(jobHasScript && jobHasTrigger); + message = "Job '${name}' in pipeline '${pipelineName}' cannot have both 'script' and 'trigger' defined."; + } + ]; finalConfig = cilib.mkJobPatched { key = name; nixConfig = config.nix; diff --git a/lib/impl/modules/pipeline.nix b/lib/impl/modules/pipeline.nix index 32b9e0c..95a6677 100644 --- a/lib/impl/modules/pipeline.nix +++ b/lib/impl/modules/pipeline.nix @@ -2,9 +2,10 @@ lib, cilib, jobSubmodule, + pkgs, ... }: let - inherit (lib) mkOption types filterAttrs mergeAttrsList mapAttrs mkRenamedOptionModule literalExpression; + inherit (lib) mkOption types filterAttrs mergeAttrsList mapAttrs mkRenamedOptionModule literalExpression attrValues; inherit (cilib.helpers) filterUnset unset mkUnsetOption toYaml toYamlPretty eitherWithSubOptions; pipelineConfigSubmodule = {rootConfig, ...}: { @@ -153,7 +154,7 @@ in { _file = ./pipeline.nix; # from https://docs.gitlab.com/ci/yaml/#default - imports = builtins.map (val: mkRenamedOptionModule [val] ["default" val]) [ + imports = [(pkgs.path + /nixos/modules/misc/assertions.nix)] ++ builtins.map (val: mkRenamedOptionModule [val] ["default" val]) [ "after_script" "artifacts" "before_script" @@ -211,16 +212,20 @@ // gitlabOptions; config = let attrsToKeep = builtins.attrNames gitlabOptions; + jobs = config.jobs; + # Propagate job assertions up to pipeline level + jobAssertions = lib.concatLists (map (job: job.assertions) (attrValues jobs)); in { + assertions = jobAssertions; finalConfig = (filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config)) - // mapAttrs (_name: value: value.finalConfig) config.jobs; + // mapAttrs (_name: value: value.finalConfig) jobs; packages = { "gitlab-ci:pipeline:${name}" = toYaml "gitlab-ci-config.json" config.finalConfig; "gitlab-ci:pipeline:${name}:pretty" = toYamlPretty "gitlab-ci-config.yml" config.finalConfig; } - // mergeAttrsList (map (job: job.packages) (builtins.attrValues config.jobs)); + // mergeAttrsList (map (job: job.packages) (builtins.attrValues jobs)); }; }; in { diff --git a/lib/impl/modules/root.nix b/lib/impl/modules/root.nix index bd99050..a520f85 100644 --- a/lib/impl/modules/root.nix +++ b/lib/impl/modules/root.nix @@ -2,9 +2,10 @@ lib, soonixSubmodule, pipelineSubmodule, + pkgs, ... }: let - inherit (lib) mkOption types foldr; + inherit (lib) mkOption types foldr mapAttrsToList concatLists; in rec { configSubmodule = { options = { @@ -28,6 +29,7 @@ in rec { }; nixCiSubmodule = {config, ...}: { + imports = [(pkgs.path + /nixos/modules/misc/assertions.nix)]; options = { config = mkOption { description = '' @@ -65,6 +67,10 @@ in rec { }; }; config = { + # Propagate pipeline assertions to the root level + assertions = concatLists ( + mapAttrsToList (_name: pipeline: pipeline.assertions) config.pipelines + ); packages = foldr (pipeline: acc: acc // pipeline) {} ( map (pipeline: pipeline.packages) (builtins.attrValues config.pipelines) ); diff --git a/tests/modules_test.nix b/tests/modules_test.nix index 8ccbd83..7b49e70 100644 --- a/tests/modules_test.nix +++ b/tests/modules_test.nix @@ -98,6 +98,59 @@ assert_file_contains ${package} 'export EXAMPLE="/nix/store/.*-hello-.*"' ''; } + { + name = "trigger job without script is valid"; + expected = { + trigger = "some/project"; + stage = "test"; + image = "$NIX_CI_IMAGE"; + }; + actual = + (cilib.mkCI { + pipelines."test" = { + stages = ["test"]; + jobs."trigger_job" = { + stage = "test"; + trigger = "some/project"; + nix.enable = false; + }; + }; + }).pipelines."test".finalConfig."trigger_job"; + } + { + name = "script job without trigger is valid"; + expected = { + stage = "test"; + image = "$NIX_CI_IMAGE"; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + script = ["echo hello"]; + after_script = ["finalize_nix_ci"]; + }; + actual = + (cilib.mkCI { + pipelines."test" = { + stages = ["test"]; + jobs."test" = { + stage = "test"; + script = ["echo hello"]; + }; + }; + }).pipelines."test".finalConfig."test"; + } + { + name = "job cannot have both script and trigger"; + expected = false; + actual = (builtins.tryEval (cilib.mkCI { + pipelines."test" = { + stages = ["test"]; + jobs."bad" = { + stage = "test"; + script = ["echo bad"]; + trigger = "some/project"; + }; + }; + })).success; + } ]; }; }