{ flake-parts-lib, lib, ... }: { options.perSystem = flake-parts-lib.mkPerSystemOption ( { config, pkgs, ... }: let cfg = config.ci.config; filterAttrsRec = pred: v: if lib.isAttrs v then lib.filterAttrs pred (lib.mapAttrs (path: filterAttrsRec pred) v) else v; subType = options: lib.types.submodule {inherit options;}; mkNullOption = type: lib.mkOption { default = null; type = lib.types.nullOr type; }; configType = with lib; subType { default-nix-image = mkOption { type = types.str; default = "registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:latest"; description = "The image to use on nix jobs"; }; nix-jobs-per-default = mkOption { type = types.bool; default = true; description = "Handle jobs nix-based by default or via opt-in (in a job set nix.enable = true) if false"; }; disable-cache = mkOption { type = types.bool; default = false; description = "Whether to remove the cache key from all nix jobs and set NIX_CI_DISABLE_CACHE"; }; cache-key = mkOption { type = types.str; default = "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"; description = "Cache key to use for the nix cache"; }; }; jobType = with lib; subType { # nix ci opts nix = mkOption { type = subType { enable = mkOption { type = types.bool; default = cfg.nix-jobs-per-default; description = "Handle this job as a nix job"; }; deps = mkOption { type = types.listOf types.package; default = []; description = "Dependencies/packages to install for this job"; }; disable-cache = mkOption { type = types.bool; default = cfg.disable-cache; description = "Whether to remove the cache key from this job and set NIX_CI_DISABLE_CACHE"; }; cache-key = mkOption { type = types.str; default = cfg.cache-key; description = "Cache key to use for the nix cache"; }; }; default = {}; description = "Configure Nix Gitlab CI for each job individually"; }; # gitlab opts script = mkOption { type = types.listOf types.str; default = []; }; stage = mkOption { type = types.str; default = "test"; }; image = mkOption { type = types.str; default = cfg.default-nix-image; }; after_script = mkNullOption (types.listOf types.str); allow_failure = mkNullOption (types.either types.attrs types.bool); artifacts = mkNullOption (types.attrs); before_script = mkNullOption (types.listOf types.str); cache = mkNullOption (types.either (types.listOf types.attrs) types.attrs); coverage = mkNullOption (types.str); dependencies = mkNullOption (types.listOf types.str); environment = mkNullOption (types.either types.attrs types.str); extends = mkNullOption (types.str); hooks = mkNullOption (types.attrs); id_tokens = mkNullOption (types.attrs); "inherit" = mkNullOption (types.attrs); interruptible = mkNullOption (types.bool); needs = mkNullOption (types.listOf (types.either types.str types.attrs)); publish = mkNullOption (types.str); pages = mkNullOption (types.attrs); parallel = mkNullOption (types.either types.int types.attrs); release = mkNullOption (types.attrs); retry = mkNullOption (types.either types.int types.attrs); rules = mkNullOption (types.listOf types.attrs); resource_group = mkNullOption (types.str); secrets = mkNullOption (types.attrs); services = mkNullOption (types.listOf types.attrs); start_in = mkNullOption (types.str); tags = mkNullOption (types.listOf types.str); timeout = mkNullOption (types.str); variables = mkNullOption (types.attrs); when = mkNullOption (types.str); }; in { options = with lib; { ci = mkOption { type = subType { config = mkOption { type = configType; description = '' Configuration options for the nix part itself ''; default = {}; }; image = mkNullOption (types.str); variables = mkNullOption (types.attrs); default = mkNullOption (types.attrs); stages = mkNullOption (types.listOf types.str); include = mkNullOption (types.attrs); workflow = mkNullOption (types.attrs); jobs = mkOption { type = types.lazyAttrsOf jobType; default = {}; }; }; description = '' Generate a Gitlab CI configuration which can be used to trigger a child pipeline. This will inject code which pre-downloads the nix deps before each job and adds them to PATH. ''; default = {}; }; }; config.packages = let toYaml = (pkgs.formats.yaml {}).generate; mapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); prepend = key: arr: job: job // lib.optionalAttrs job.nix.enable { ${key} = arr ++ (job.${key} or []); }; append = key: arr: job: job // lib.optionalAttrs job.nix.enable { ${key} = (job.${key} or []) ++ arr; }; prependToBeforeScript = prepend "before_script"; appendToAfterScript = append "after_script"; # filter job's variables to either only those containing store paths # or those that do not filterJobVariables = nix: job: lib.concatMapAttrs ( name: value: lib.optionalAttrs ((lib.hasInfix "/nix/store/" value) == nix) { ${name} = value; } ) (job.variables or {}); jobs = filterAttrsRec (n: v: v != null) config.ci.jobs; rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs config.ci ["jobs" "config"]); # this allows us to nix build this to get all the mentioned dependencies from the binary cache # pro: we don't have to download everything, just the deps for the current job # before, we just allowed pkgs inside the script string directly, but now with the ability to source this file # we can support different architectures between runners (eg. the arch of the initial runner does not matter) jobsMappedForDeps = mapAttrs (key: job: let variablesWithStorePaths = filterJobVariables true job; variableExports = lib.concatLines ( lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths ); in { name = "gitlab-ci-job-deps:${key}"; value = pkgs.writeShellScript "gitlab-ci-job-deps:${key}" '' export PATH="${lib.makeBinPath job.nix.deps}:$PATH"; ${variableExports} ''; }) jobs; # allows the user to directly run the script jobsMappedForScript = mapAttrs (key: job: let variablesWithStorePaths = filterJobVariables false job; variableExports = lib.concatLines ( lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths ); in { name = "gitlab-ci-job:${key}"; value = pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' # set up deps and environment variables containing store paths . ${jobsMappedForDeps."gitlab-ci-job-deps:${key}"} # normal environment variables ${variableExports} # run before_script, script and after_script echo -e "\e[32mRunning before_script...\e[0m" ${lib.concatLines (job.before_script or [])} echo -e "\e[32mRunning script...\e[0m" ${lib.concatLines job.script} echo -e "\e[32mRunning after_script...\e[0m" ${lib.concatLines (job.after_script or [])} ''; }) jobs; # build the deps specific for this job before anything, this way the deps should be fetched from the cache jobsPatched = mapAttrs (key: job: { name = key; value = assert lib.assertMsg (builtins.elem job.stage (rest.stages or [])) "stage '${job.stage}' of job '${key}' does not exist"; builtins.removeAttrs ( (prependToBeforeScript [ "source setup_nix_ci ${key}" ] (appendToAfterScript [ "finalize_nix_ci" ] job)) // lib.optionalAttrs job.nix.enable { image = job.image; variables = (filterJobVariables false job) // lib.optionalAttrs job.nix.disable-cache { NIX_CI_DISABLE_CACHE = "yes"; }; cache = ( let c = job.cache or []; in if builtins.isList c then c else [c] ) ++ (lib.optional (!job.nix.disable-cache) { key = job.nix.cache-key; paths = [".nix-cache/"]; }); } ) ["nix"]; }) jobs; in { gitlab-ci-config = toYaml "generated-gitlab-ci.yml" (rest // jobsPatched); } // jobsMappedForDeps // jobsMappedForScript; } ); }