From 0f62d5e8221e9462d731aa57270258f00cd63965 Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 16 Sep 2024 09:31:35 +0000 Subject: [PATCH 001/102] fix: allow jobs to omit the "nix" option --- lib/flakeModule.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index cd5216b..e518964 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -72,6 +72,7 @@ description = "Cache key to use for the nix cache"; }; }; + default = {}; description = "Configure Nix Gitlab CI for each job individually"; }; # gitlab opts From 6448bf59164a2e66aa8bfe5ed8e128ce9f4a4a8e Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 16 Sep 2024 09:41:08 +0000 Subject: [PATCH 002/102] chore(README): bump component version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22d082e..4b07514 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.0 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.1 inputs: # specify inputs here, for example: image_tag: latest-cachix From fdb6193ef1053b7a7a86b6b84ec8c9b697960854 Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 2 Oct 2024 18:26:14 +0200 Subject: [PATCH 003/102] fix(utils): migrate utils to the new syntax aswell --- lib/utils.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.nix b/lib/utils.nix index 2fd49d0..a40dadd 100644 --- a/lib/utils.nix +++ b/lib/utils.nix @@ -40,7 +40,7 @@ echo -e "\\e[0Ksection_end:`date +%s`:commit\\r\\e[0K" '' ]; - deps = (jobArgs.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; + nix.deps = (jobArgs.nix.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; }; }; } From 28072631e73e1dd362b5d7d292ca197ff744b77b Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 2 Oct 2024 21:40:26 +0200 Subject: [PATCH 004/102] chore(README): bump component version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b07514..ac246a0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.1 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.2 inputs: # specify inputs here, for example: image_tag: latest-cachix From ec279ae31ee77704d0d1eb3c10c0141f6a72b7e0 Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 6 Oct 2024 19:19:33 +0200 Subject: [PATCH 005/102] fix(basic-image): append ^* to .drv entries to prevent warnings instead print a "." without newline to be able to see progress --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index 4553c2d..1e7387d 100644 --- a/flake.nix +++ b/flake.nix @@ -155,7 +155,9 @@ '') (finalizeScript '' while read entry; do + [[ "$entry" == *.drv ]] && entry+="^*" || true nix copy --quiet --to "file://$(pwd)/.nix-cache" $entry || true + echo -n "." done '') ]; From a22258583de9bd0d2558821d40d98a5e4fc1f7c6 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Mon, 7 Oct 2024 16:59:30 +0000 Subject: [PATCH 006/102] ci: improve dogfooding by using the CI built images directly --- .gitlab-ci.yml | 5 ++++- flake.nix | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ffc5b2..b0393fb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,13 @@ include: - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA + inputs: + image_tag: $CI_COMMIT_SHORT_SHA stages: + - build-images - build - trigger build:image: - stage: build + stage: build-images parallel: matrix: - VARIANT: ["", "-cachix", "-attic"] diff --git a/flake.nix b/flake.nix index 4553c2d..75868e4 100644 --- a/flake.nix +++ b/flake.nix @@ -50,9 +50,6 @@ jobs = { "test" = { stage = "test"; - # wait an hour so the image builds - when = "delayed"; - start_in = "1 hour"; nix = { deps = [pkgs.hello pkgs.curl]; disable-cache = false; From a47ec8cbeb634281ab8400ce0e7f269748c38d58 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 8 Oct 2024 12:33:31 +0200 Subject: [PATCH 007/102] chore(CI): use image built in parent pipeline for dogfooding here aswell remove retries --- flake.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 7d198ed..902482c 100644 --- a/flake.nix +++ b/flake.nix @@ -40,13 +40,9 @@ }; }; ci = { + # use the image built in the parent pipeline for dogfooding + config.default-nix-image = "registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$CI_COMMIT_SHORT_SHA"; stages = ["test"]; - default = { - retry = { - max = 2; - when = "runner_system_failure"; - }; - }; jobs = { "test" = { stage = "test"; From 67d75bd167813fe81af7b80aba86a9fe67e042ff Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 13 Oct 2024 15:50:18 +0200 Subject: [PATCH 008/102] docs(README): document running jobs locally [skip ci] --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ac246a0..384aff2 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,21 @@ anything non-empty (eg. "yes") when triggering the pipeline. The `build:nix-ci` job has a different special environment variable `NIX_CI_SKIP_CACHE` (useful if the generated pipeline is outdated but caching should generally still take place). +### Run Jobs locally + +You can run any job's script (+ before and after) locally with Nix for easier testing: + +```sh +nix run .#gitlab-ci-job: +``` + +There is also `.#gitlab-ci-job-deps:` which generates and exports the required environment variables for each job: + +- PATH (with all deps) +- any custom env variables which contain store paths to not break stuff when switching archs + +Please see #8 for some issues and further improvements on this. + ## Thanks to Some parts of this implementation are adapted/inspired from https://gitlab.com/Cynerd/gitlab-ci-nix From 81b497d9764c0dc2aa7a351f70690f35f0eaccb3 Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 13 Oct 2024 16:35:14 +0200 Subject: [PATCH 009/102] chore(README): bump component version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 384aff2..c72c569 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.2 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.3 inputs: # specify inputs here, for example: image_tag: latest-cachix From 532fb8002c441d38ae7943e4366fe2c1dfa0c862 Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 13 Oct 2024 18:32:47 +0200 Subject: [PATCH 010/102] fix(module): append "finalize_nix_ci" to after_script, not prepend --- lib/flakeModule.nix | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index e518964..891b825 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -155,10 +155,15 @@ // lib.optionalAttrs job.nix.enable { ${key} = arr - ++ job.${key} or []; + ++ (job.${key} or []); + }; + append = key: arr: job: + job + // lib.optionalAttrs job.nix.enable { + ${key} = (job.${key} or []) ++ arr; }; prependToBeforeScript = prepend "before_script"; - prependToAfterScript = prepend "after_script"; + appendToAfterScript = append "after_script"; jobs = filterAttrsRec (n: v: v != null) config.ci.jobs; rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs config.ci ["jobs" "config"]); @@ -203,7 +208,7 @@ (prependToBeforeScript [ "source setup_nix_ci ${key}" ] - (prependToAfterScript [ + (appendToAfterScript [ "finalize_nix_ci" ] job)) From 25e5b44a6dfc19505a54b939498dc51c27a1acac Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 13 Oct 2024 18:34:07 +0200 Subject: [PATCH 011/102] feat(module): improve ability to run jobs locally adds the correct PATH and environment variables to run it locally similar to how it works in CI. Also split up the before_script, script and after_script with simple echo's inbetween for easier debugging --- lib/flakeModule.nix | 49 ++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 891b825..db90a9b 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -165,6 +165,17 @@ 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 @@ -173,15 +184,8 @@ # we can support different architectures between runners (eg. the arch of the initial runner does not matter) jobsMappedForDeps = mapAttrs (key: job: let - variablesWithStorePaths = - lib.concatMapAttrs ( - name: value: - lib.optionalAttrs (lib.hasInfix "/nix/store/" value) { - ${name} = value; - } - ) - (job.variables or {}); - variableExports = lib.concatMapStrings (x: "${x}\n") ( + variablesWithStorePaths = filterJobVariables true job; + variableExports = lib.concatLines ( lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths ); in { @@ -194,9 +198,26 @@ jobs; # allows the user to directly run the script jobsMappedForScript = - mapAttrs (key: job: { + 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}" (lib.strings.concatLines (job.before_script or [] ++ job.script ++ job.after_script or [])); + 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 @@ -215,11 +236,7 @@ // lib.optionalAttrs job.nix.enable { image = job.image; variables = - lib.concatMapAttrs (name: value: - lib.optionalAttrs (!lib.hasInfix "/nix/store/" value) { - ${name} = value; - }) - (job.variables or {}) + (filterJobVariables false job) // lib.optionalAttrs job.nix.disable-cache { NIX_CI_DISABLE_CACHE = "yes"; }; From 691a0aac7956ada011e3fd3a01bd5d3ae850270b Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Wed, 30 Oct 2024 16:10:18 +0000 Subject: [PATCH 012/102] chore: add LICENSE [skip ci] --- LICENSE | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57fd6fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 TECHNOFAB + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 50f0184a7b6e71de301911b5a9da17aa5057186d Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Wed, 30 Oct 2024 16:11:19 +0000 Subject: [PATCH 013/102] chore(LICENSE): use markdown for better readability [skip ci] --- LICENSE => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.md (100%) diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md From 856924acdf0450c870897f7928d1e5f405b25f98 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 26 Nov 2024 13:31:04 +0100 Subject: [PATCH 014/102] chore(module): add packages to legacyPackages instead --- lib/flakeModule.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index db90a9b..64f7926 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -147,7 +147,7 @@ }; }; - config.packages = let + config.legacyPackages = 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: From a85c4e49fe3cc1fb0060334c0adf3b2f453ee626 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 26 Nov 2024 14:11:45 +0100 Subject: [PATCH 015/102] chore: add additional NIX_CONFIG var & reformat closes #11 --- flake.nix | 84 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/flake.nix b/flake.nix index 902482c..2b54703 100644 --- a/flake.nix +++ b/flake.nix @@ -87,12 +87,12 @@ extra-trusted-public-keys = $NIX_PUBLIC_KEYS extra-trusted-substituters = $NIX_SUBSTITUTERS extra-substituters = $NIX_SUBSTITUTERS + $NIX_CONFIG $NIX_EXTRA_CONFIG " echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" - ${ - "" # load the job's deps only if the name was passed - } + + # load the job's deps only if the name was passed if [[ ! -z $1 ]]; then echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching deps for job" nix build .#gitlab-ci-job-deps:$1 @@ -138,36 +138,64 @@ }; }; in { - setup-script = setupScript "true # extra_setup"; - finalize-script = finalizeScript "true # push_command"; + setup-script = + setupScript + # sh + '' + # extra_setup + true + ''; + finalize-script = + finalizeScript + # sh + '' + # push_command + true + ''; image = mkImage [ - (setupScript '' - cachedir="$(pwd)/.nix-cache" - echo "Configuring caching with the Runner Cache in $cachedir..." - export NIX_SUBSTITUTERS="$NIX_SUBSTITUTERS file://$cachedir?priority=10&trusted=true" - '') - (finalizeScript '' - while read entry; do - [[ "$entry" == *.drv ]] && entry+="^*" || true - nix copy --quiet --to "file://$(pwd)/.nix-cache" $entry || true - echo -n "." - done - '') + (setupScript + # sh + '' + cachedir="$(pwd)/.nix-cache" + echo "Configuring caching with the Runner Cache in $cachedir..." + export NIX_SUBSTITUTERS="$NIX_SUBSTITUTERS file://$cachedir?priority=10&trusted=true" + '') + (finalizeScript + # sh + '' + while read entry; do + [[ "$entry" == *.drv ]] && entry+="^*" || true + nix copy --quiet --to "file://$(pwd)/.nix-cache" $entry || true + echo -n "." + done + '') ]; image-cachix = mkImage [ - (setupScript '' - echo "Configuring caching with cachix..." - ${pkgs.cachix}/bin/cachix use $CACHIX_CACHE || true - '') - (finalizeScript "${pkgs.cachix}/bin/cachix push $CACHIX_CACHE || true") + (setupScript + # sh + '' + echo "Configuring caching with cachix..." + ${pkgs.cachix}/bin/cachix use $CACHIX_CACHE || true + '') + (finalizeScript + # sh + '' + ${pkgs.cachix}/bin/cachix push $CACHIX_CACHE || true + '') ]; image-attic = mkImage [ - (setupScript '' - echo "Configuring caching with attic..." - ${inputs'.attic.packages.attic-client}/bin/attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true - ${inputs'.attic.packages.attic-client}/bin/attic use "$ATTIC_CACHE" || true - '') - (finalizeScript "${inputs'.attic.packages.attic-client}/bin/attic push ci:$ATTIC_CACHE || true") + (setupScript + # sh + '' + echo "Configuring caching with attic..." + ${inputs'.attic.packages.attic-client}/bin/attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true + ${inputs'.attic.packages.attic-client}/bin/attic use "$ATTIC_CACHE" || true + '') + (finalizeScript + # sh + '' + ${inputs'.attic.packages.attic-client}/bin/attic push ci:$ATTIC_CACHE || true + '') ]; }; From 026a7549dfb4691979f8bfe354e531ca70ccbb64 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 26 Nov 2024 20:22:11 +0100 Subject: [PATCH 016/102] chore: update flake inputs and use attic-client from nixpkgs --- flake.lock | 335 +++++++++++++++++++++++++---------------------------- flake.nix | 16 ++- 2 files changed, 163 insertions(+), 188 deletions(-) diff --git a/flake.lock b/flake.lock index f1d2356..dd864f4 100644 --- a/flake.lock +++ b/flake.lock @@ -1,65 +1,49 @@ { "nodes": { - "attic": { + "cachix": { "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": [ - "nixpkgs" + "devenv": [ + "devenv" ], - "nixpkgs-stable": "nixpkgs-stable" + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv" + ], + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1710680783, - "narHash": "sha256-exrsUUk/VSCG2Y3Sr/dkST8vwICzab3fe8Je81t/SfM=", - "owner": "TECHNOFAB", - "repo": "attic", - "rev": "b6d8d5ca53ef15910042a39e81f35383370c1112", - "type": "gitlab" - }, - "original": { - "owner": "TECHNOFAB", - "repo": "attic", - "type": "gitlab" - } - }, - "crane": { - "inputs": { - "nixpkgs": [ - "attic", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1702918879, - "narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=", - "owner": "ipetkov", - "repo": "crane", - "rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb", + "lastModified": 1728672398, + "narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=", + "owner": "cachix", + "repo": "cachix", + "rev": "aac51f698309fd0f381149214b7eee213c66ef0a", "type": "github" }, "original": { - "owner": "ipetkov", - "repo": "crane", + "owner": "cachix", + "ref": "latest", + "repo": "cachix", "type": "github" } }, "devenv": { "inputs": { - "flake-compat": "flake-compat_2", + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": [ + "git-hooks" + ], "nix": "nix", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "pre-commit-hooks" - ] + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1705094340, - "narHash": "sha256-T7d1d5PQXqDbZUnKjRTBf26yTCMSttDQULM8jU4jiro=", + "lastModified": 1732585607, + "narHash": "sha256-6ffeaSMuaL326f7KrCeScpSJtdHsFKS9gPrsSZkndvU=", "owner": "cachix", "repo": "devenv", - "rev": "96b49eb381779bdd4f41ca176a762867c31db5da", + "rev": "a520f05c40ebecaf5e17064b27e28ba8e70c49fb", "type": "github" }, "original": { @@ -71,11 +55,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -85,22 +69,6 @@ } }, "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-compat_3": { "flake": false, "locked": { "lastModified": 1696426674, @@ -118,14 +86,18 @@ }, "flake-parts": { "inputs": { - "nixpkgs-lib": "nixpkgs-lib" + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] }, "locked": { - "lastModified": 1704982712, - "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "07f6395285469419cf9d078f59b5b49993198c00", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", "type": "github" }, "original": { @@ -134,25 +106,49 @@ "type": "github" } }, - "flake-utils": { + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat_2", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_4", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1732021966, + "narHash": "sha256-mnTbjpdqF0luOkou8ZFi2asa1N3AA2CchR/RqCNmsGE=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "3308484d1a443fc5bc92012435d79e80458fe43c", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", "type": "github" } }, "gitignore": { "inputs": { "nixpkgs": [ - "pre-commit-hooks", + "git-hooks", "nixpkgs" ] }, @@ -170,119 +166,90 @@ "type": "github" } }, - "lowdown-src": { + "libgit2": { "flake": false, "locked": { - "lastModified": 1633514407, - "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=", - "owner": "kristapsdz", - "repo": "lowdown", - "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8", + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", "type": "github" }, "original": { - "owner": "kristapsdz", - "repo": "lowdown", + "owner": "libgit2", + "repo": "libgit2", "type": "github" } }, "nix": { "inputs": { - "lowdown-src": "lowdown-src", - "nixpkgs": [ - "devenv", - "nixpkgs" + "flake-compat": [ + "devenv" ], - "nixpkgs-regression": "nixpkgs-regression" + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ], + "pre-commit-hooks": [ + "devenv" + ] }, "locked": { - "lastModified": 1676545802, - "narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=", + "lastModified": 1727438425, + "narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=", "owner": "domenkozar", "repo": "nix", - "rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f", + "rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546", "type": "github" }, "original": { "owner": "domenkozar", - "ref": "relaxed-flakes", + "ref": "devenv-2.24", "repo": "nix", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1678875422, - "narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=", + "lastModified": 1730531603, + "narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459", + "rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "dir": "lib", - "lastModified": 1703961334, - "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", - "type": "github" - }, - "original": { - "dir": "lib", "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, - "nixpkgs-regression": { + "nixpkgs-lib": { "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" + "lastModified": 1730504152, + "narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" }, "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" } }, "nixpkgs-stable": { "locked": { - "lastModified": 1702780907, - "narHash": "sha256-blbrBBXjjZt6OKTcYX1jpe9SRof2P9ZYWPzq22tzXAA=", + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1e2e384c5b7c50dbf8e9c441a9e58d85f408b01f", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-stable_2": { - "locked": { - "lastModified": 1720386169, - "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", "type": "github" }, "original": { @@ -294,27 +261,43 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1705204527, - "narHash": "sha256-WVz9WdaFBhAwO/7A+HlW8HPJ4VQ8QnpCD1WZAcAPneo=", + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dd5621df6dcb90122b50da5ec31c411a0de3e538", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "release-24.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_3": { "locked": { - "lastModified": 1719082008, - "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "lastModified": 1716977621, + "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1730768919, + "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", "type": "github" }, "original": { @@ -324,50 +307,44 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { - "lastModified": 1725103162, - "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", - "owner": "nixos", + "lastModified": 1732238832, + "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", + "rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat_3", - "gitignore": "gitignore", - "nixpkgs": "nixpkgs_3", - "nixpkgs-stable": "nixpkgs-stable_2" - }, + "nixpkgs_6": { "locked": { - "lastModified": 1725513492, - "narHash": "sha256-tyMUA6NgJSvvQuzB7A1Sf8+0XCHyfSPRx/b00o6K0uo=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "7570de7b9b504cfe92025dd1be797bf546f66528", + "lastModified": 1731890469, + "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5083ec887760adfe12af64830a66807423a859a7", "type": "github" }, "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "attic": "attic", "devenv": "devenv", - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs_2", - "pre-commit-hooks": "pre-commit-hooks", + "flake-parts": "flake-parts_2", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs_5", "systems": "systems", "treefmt-nix": "treefmt-nix" } @@ -389,14 +366,14 @@ }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_6" }, "locked": { - "lastModified": 1725271838, - "narHash": "sha256-VcqxWT0O/gMaeWTTjf1r4MOyG49NaNxW4GHTO3xuThE=", + "lastModified": 1732643199, + "narHash": "sha256-uI7TXEb231o8dkwB5AUCecx3AQtosRmL6hKgnckvjps=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "9fb342d14b69aefdf46187f6bb80a4a0d97007cd", + "rev": "84637a7ab04179bdc42aa8fd0af1909fba76ad0c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2b54703..7bb020c 100644 --- a/flake.nix +++ b/flake.nix @@ -188,13 +188,13 @@ # sh '' echo "Configuring caching with attic..." - ${inputs'.attic.packages.attic-client}/bin/attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true - ${inputs'.attic.packages.attic-client}/bin/attic use "$ATTIC_CACHE" || true + ${pkgs.attic-client}/bin/attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true + ${pkgs.attic-client}/bin/attic use "$ATTIC_CACHE" || true '') (finalizeScript # sh '' - ${inputs'.attic.packages.attic-client}/bin/attic push ci:$ATTIC_CACHE || true + ${pkgs.attic-client}/bin/attic push --stdin ci:$ATTIC_CACHE || true '') ]; }; @@ -211,25 +211,23 @@ systems.url = "github:nix-systems/default-linux"; devenv = { url = "github:cachix/devenv"; - inputs.pre-commit-hooks.follows = "pre-commit-hooks"; + inputs.git-hooks.follows = "git-hooks"; }; - pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix"; + git-hooks.url = "github:cachix/git-hooks.nix"; treefmt-nix.url = "github:numtide/treefmt-nix"; - attic = { - url = "gitlab:TECHNOFAB/attic"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; nixConfig = { extra-substituters = [ "https://cache.nixos.org/" "https://nix-community.cachix.org" + "https://devenv.cachix.org" ]; extra-trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" ]; }; } From 90a407920040c95e2a279ac55879ccc520c6ee36 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Wed, 27 Nov 2024 13:15:50 +0000 Subject: [PATCH 017/102] chore(image): pass all store paths at once via stdin to nix copy --- flake.nix | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 2b54703..6a9bb2a 100644 --- a/flake.nix +++ b/flake.nix @@ -163,11 +163,8 @@ (finalizeScript # sh '' - while read entry; do - [[ "$entry" == *.drv ]] && entry+="^*" || true - nix copy --quiet --to "file://$(pwd)/.nix-cache" $entry || true - echo -n "." - done + # add ^* to all store paths ending in .drv (prevent warning log spam) + ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "file://$(pwd)/.nix-cache" --stdin || true '') ]; image-cachix = mkImage [ From 553b829af0f38c5aefb2114ffbf7f382dcc77acd Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 30 Nov 2024 15:13:22 +0100 Subject: [PATCH 018/102] chore(README): bump component version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c72c569..4b24815 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.0.3 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.1.0 inputs: # specify inputs here, for example: image_tag: latest-cachix From 1101989255903a1f20781e007da3b313b9277173 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 30 Nov 2024 17:57:31 +0100 Subject: [PATCH 019/102] fix(ci-component): honor NIX_CI_DISABLE_CACHE correctly --- README.md | 2 +- templates/nix-gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b24815..b9a8159 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.1.0 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.1.1 inputs: # specify inputs here, for example: image_tag: latest-cachix diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 425d203..627d15c 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -20,7 +20,7 @@ stages: - build - trigger variables: - NIX_CI_DISABLE_CACHE: "$[[ inputs.disable_cache ]]" + NIX_CI_DISABLE_CACHE: "$[[ inputs.disable_cache ]]${NIX_CI_DISABLE_CACHE:-}" nix-ci:build: stage: build image: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$[[ inputs.image_tag ]] From 016e6c9dc70c337aa8e0a9add221dd1b1abb43bb Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Sat, 15 Feb 2025 21:35:36 +0100 Subject: [PATCH 020/102] =?UTF-8?q?chore!:=20rename=20NIX=5FCI=5FSKIP=5FCA?= =?UTF-8?q?CHE=20=E2=86=92=20NIX=5FCI=5FFORCE=5FBUILD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #16 --- README.md | 6 +++--- templates/nix-gitlab-ci.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b9a8159..9735feb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@1.1.1 + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: use the latest version (try not to use latest) inputs: # specify inputs here, for example: image_tag: latest-cachix @@ -55,8 +55,8 @@ include: To disable any of the provided caches for a pipeline one can set `NIX_CI_DISABLE_CACHE` to anything non-empty (eg. "yes") when triggering the pipeline. -The `build:nix-ci` job has a different special environment variable `NIX_CI_SKIP_CACHE` -(useful if the generated pipeline is outdated but caching should generally still take place). +The `build:nix-ci` job has a different special environment variable `NIX_CI_FORCE_BUILD` +(useful if the generated pipeline in the cache is outdated, this will build it again). ### Run Jobs locally diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 627d15c..911556a 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -34,9 +34,9 @@ nix-ci:build: - .nix-cache/ before_script: # generated-gitlab-ci.yml exists in the cache - - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "Using cached pipeline file (skip cache with NIX_CI_SKIP_CACHE)" || true' + - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - - '[ -n "$NIX_CI_SKIP_CACHE" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_SKIP_CACHE)" || true' + - '[ -n "$NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' # only setup when we need to generate the pipeline yaml - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' script: From 586fb88b9d51a662431a4281a2f66274b1d2c919 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 12:24:54 +0100 Subject: [PATCH 021/102] feat(v2): initial v2 implementation add multi-arch (arm & x64) image add multiple pipelines (ci now creates the "default" pipeline as a shorthand) simplify devenv flake input merge all cache options together, now $NIX_CI_CACHE_STRATEGY decides how the cache works setup_nix_ci and finalize_nix_ci are now flake packages and work standalone the specific image is not needed anymore, any image with the right dependencies works runner cache is not the default anymore (because it sucked most of the time) the pipeline is selected by $NIX_CI_PIPELINE_NAME or if empty by $CI_PIPELINE_SOURCE, so for the old behaviour $NIX_CI_PIPELINE_NAME=default is needed, future work will be needed to handle this more nicely --- .gitlab-ci.yml | 34 ++- README.md | 16 +- flake.lock | 76 ++----- flake.nix | 240 ++++++++++---------- lib/flakeModule.nix | 425 +++++++++++++++++++----------------- templates/nix-gitlab-ci.yml | 30 ++- 6 files changed, 409 insertions(+), 412 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b0393fb..84edac4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,26 +10,40 @@ build:image: stage: build-images parallel: matrix: - - VARIANT: ["", "-cachix", "-attic"] + - ARCH: ["x86_64-linux", "aarch64-linux"] image: nixpkgs/nix-flakes:latest before_script: - - nix profile install nixpkgs#skopeo + - nix profile install nixpkgs#buildah - export PATH="$PATH:$HOME/.nix-profile/bin" script: - - nix build .#image${VARIANT} + - nix build .#image --system $ARCH + after_script: + - install -D result dist/nix-ci-$ARCH.tar.gz + artifacts: + paths: + - dist +deploy:image: + stage: build-images + needs: + - build:image + before_script: + - export REGISTRY_AUTH_FILE=''${HOME}/auth.json + - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + - mkdir -p /etc/containers && echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json + - mkdir -p /var/tmp + script: - export NORMALIZED_BRANCH=${CI_COMMIT_BRANCH/\//-} - - skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}" --tmpdir /tmp "docker-archive:result" "docker://$CI_REGISTRY_IMAGE/nix-ci:${CI_COMMIT_SHORT_SHA}${VARIANT}" + - buildah manifest create localhost/nix-ci + - buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-x86_64-linux.tar.gz + - buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-aarch64-linux.tar.gz + - buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_SHORT_SHA} # branches - | if [ -z "$CI_COMMIT_TAG" ]; then - skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}" --tmpdir /tmp \ - "docker-archive:result" \ - "docker://$CI_REGISTRY_IMAGE/nix-ci:${NORMALIZED_BRANCH/main/latest}${VARIANT}"; + buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${NORMALIZED_BRANCH/main/latest} fi # tags - | if [ -n "$CI_COMMIT_TAG" ]; then - skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}" --tmpdir /tmp \ - "docker-archive:result" \ - "docker://$CI_REGISTRY_IMAGE/nix-ci:${CI_COMMIT_TAG}${VARIANT}"; + buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_TAG} fi diff --git a/README.md b/README.md index 9735feb..da2b36b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Also makes it possible to split CI parts in a separate module which can be impor ... perSystem = {pkgs, ...}: { + # ci is a shortcut and creates a "default" pipeline ci = { stages = ["test"]; jobs = { @@ -33,6 +34,11 @@ Also makes it possible to split CI parts in a separate module which can be impor }; }; }; + # runs on a merge request for example + pipelines."merge_request_event" = { + stages = ["some_stage"]; + jobs = { ... }; + }; ... } } @@ -43,9 +49,6 @@ Also makes it possible to split CI parts in a separate module which can be impor # .gitlab-ci.yml include: - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: use the latest version (try not to use latest) - inputs: - # specify inputs here, for example: - image_tag: latest-cachix ``` ## Utilities @@ -63,16 +66,15 @@ The `build:nix-ci` job has a different special environment variable `NIX_CI_FORC You can run any job's script (+ before and after) locally with Nix for easier testing: ```sh -nix run .#gitlab-ci-job: +# / pipeline name, like "default" +nix run .#gitlab-ci:pipeline::job: ``` -There is also `.#gitlab-ci-job-deps:` which generates and exports the required environment variables for each job: +There is also `.#gitlab-ci:pipeline::job-deps:` which generates and exports the required environment variables for each job: - PATH (with all deps) - any custom env variables which contain store paths to not break stuff when switching archs -Please see #8 for some issues and further improvements on this. - ## Thanks to Some parts of this implementation are adapted/inspired from https://gitlab.com/Cynerd/gitlab-ci-nix diff --git a/flake.lock b/flake.lock index dd864f4..ff34ba8 100644 --- a/flake.lock +++ b/flake.lock @@ -32,9 +32,7 @@ "inputs": { "cachix": "cachix", "flake-compat": "flake-compat", - "git-hooks": [ - "git-hooks" - ], + "git-hooks": "git-hooks", "nix": "nix", "nixpkgs": "nixpkgs_3" }, @@ -68,22 +66,6 @@ "type": "github" } }, - "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -126,17 +108,21 @@ }, "git-hooks": { "inputs": { - "flake-compat": "flake-compat_2", + "flake-compat": [ + "devenv" + ], "gitignore": "gitignore", - "nixpkgs": "nixpkgs_4", - "nixpkgs-stable": "nixpkgs-stable" + "nixpkgs": [ + "devenv", + "nixpkgs" + ] }, "locked": { - "lastModified": 1732021966, - "narHash": "sha256-mnTbjpdqF0luOkou8ZFi2asa1N3AA2CchR/RqCNmsGE=", + "lastModified": 1737465171, + "narHash": "sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "3308484d1a443fc5bc92012435d79e80458fe43c", + "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17", "type": "github" }, "original": { @@ -148,6 +134,7 @@ "gitignore": { "inputs": { "nixpkgs": [ + "devenv", "git-hooks", "nixpkgs" ] @@ -243,22 +230,6 @@ "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" } }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1730741070, - "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, "nixpkgs_2": { "locked": { "lastModified": 1717432640, @@ -292,22 +263,6 @@ } }, "nixpkgs_4": { - "locked": { - "lastModified": 1730768919, - "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { "locked": { "lastModified": 1732238832, "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", @@ -323,7 +278,7 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_5": { "locked": { "lastModified": 1731890469, "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", @@ -343,8 +298,7 @@ "inputs": { "devenv": "devenv", "flake-parts": "flake-parts_2", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs_5", + "nixpkgs": "nixpkgs_4", "systems": "systems", "treefmt-nix": "treefmt-nix" } @@ -366,7 +320,7 @@ }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_6" + "nixpkgs": "nixpkgs_5" }, "locked": { "lastModified": 1732643199, diff --git a/flake.nix b/flake.nix index 596b693..f07964e 100644 --- a/flake.nix +++ b/flake.nix @@ -39,16 +39,15 @@ }; }; }; + # should set the "default" pipeline ci = { - # use the image built in the parent pipeline for dogfooding - config.default-nix-image = "registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$CI_COMMIT_SHORT_SHA"; stages = ["test"]; jobs = { "test" = { stage = "test"; nix = { deps = [pkgs.hello pkgs.curl]; - disable-cache = false; + enable-runner-cache = true; }; variables = { TEST = "test"; @@ -60,6 +59,11 @@ "echo $TEST $TEST_WITH_DERIVATION" ]; }; + "test-default" = { + stage = "test"; + nix.deps = [pkgs.hello]; + script = ["hello"]; + }; "test-non-nix" = { nix.enable = false; stage = "test"; @@ -70,130 +74,130 @@ }; }; }; + pipelines."non-default" = { + stages = ["test"]; + jobs = { + "test" = { + stage = "test"; + script = [ + "echo Hello from another pipeline" + ]; + }; + }; + }; packages = let - setupScript = extra_setup: - pkgs.writeShellScriptBin "setup_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" - nix path-info --all > /tmp/nix-store-before + setupScript = pkgs.writeShellScriptBin "setup_nix_ci" '' + echo -e "\\e[0Ksection_start:`date +%s`:nix_setup\\r\\e[0KSetting up Nix CI" + nix path-info --all > /tmp/nix-store-before - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - ${extra_setup} - else - echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration" - fi - - export NIX_CONFIG=" - extra-trusted-public-keys = $NIX_PUBLIC_KEYS - extra-trusted-substituters = $NIX_SUBSTITUTERS - extra-substituters = $NIX_SUBSTITUTERS - $NIX_CONFIG - $NIX_EXTRA_CONFIG - " - echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" + if [ -z "$_NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($_NIX_CI_CACHE_STRATEGY)" + case "$_NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + echo "Runner Cache: $RUNNER_CACHE" + export NIX_CONFIG="$NIX_CONFIG + extra-trusted-substituters = $RUNNER_CACHE?priority=10&trusted=true + extra-substituters = $RUNNER_CACHE?priority=10&trusted=true + " + ;; + "attic") + echo "Attic Cache: $ATTIC_CACHE" + attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true + attic use "$ATTIC_CACHE" || true + ;; + "cachix") + echo "Cachix Cache: $CACHIX_CACHE" + cachix use "$CACHIX_CACHE" || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$_NIX_CI_CACHE_STRATEGY'" + ;; + esac + echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" + else + echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration..." + fi # load the job's deps only if the name was passed if [[ ! -z $1 ]]; then - echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching deps for job" - nix build .#gitlab-ci-job-deps:$1 + echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching Nix dependencies for job" + nix build .#$1 source $(readlink -f result) echo -e "\\e[0Ksection_end:`date +%s`:nix_deps\\r\\e[0K" fi - ''; - finalizeScript = push_command: - pkgs.writeShellScriptBin "finalize_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing new store paths to cache" + echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" + ''; + finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" '' + echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci\\r\\e[0KFinalizing Nix CI..." nix path-info --all > /tmp/nix-store-after - ${pkgs.diffutils}/bin/diff --new-line-format="%L" \ + echo "Finding new paths..." + NEW_PATHS=$(${pkgs.diffutils}/bin/diff --new-line-format="%L" \ --old-line-format="" --unchanged-line-format="" \ - /tmp/nix-store-before /tmp/nix-store-after \ - | { - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - ${push_command} - else - ${pkgs.busybox}/bin/wc -l | { read count; echo "Caching disabled, not uploading $count new store entries..."; } - fi - } - echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" - ''; - mkImage = extraPackages: - pkgs.dockerTools.buildImage { - name = "nix-gitlab-ci"; - fromImage = pkgs.dockerTools.pullImage { - imageName = "nixpkgs/nix-flakes"; - imageDigest = "sha256:d88e521662cb6bf9cef006b79ed6ed1069e297171f3c2585f2b898b30f7c045c"; - sha256 = "1pcbgxz9c98mfqrzyi14h568dw8vxj1kbgirnwl6vs8wfaamjaaf"; - finalImageName = "nixpkgs/nix-flakes"; - finalImageTag = "latest"; - }; - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = - [ - pkgs.gitMinimal - pkgs.gnugrep - ] - ++ extraPackages; - pathsToLink = ["/bin"]; - }; - }; + /tmp/nix-store-before /tmp/nix-store-after) + COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") + if [ -z "$_NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($_NIX_CI_CACHE_STRATEGY)" + echo $NEW_PATHS | { + case "$_NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + # add ^* to all store paths ending in .drv (prevent warning log spam) + ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true + ;; + "attic") + attic push --stdin ci:$ATTIC_CACHE || true + ;; + "cachix") + cachix push $CACHIX_CACHE || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$_NIX_CI_CACHE_STRATEGY'" + ;; + esac + } + echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" + else + echo "Caching disabled, not uploading $COUNT new store entries..." + fi + echo -e "\\e[0Ksection_end:`date +%s`:finalize_nix_ci\\r\\e[0K" + ''; in { - setup-script = - setupScript - # sh - '' - # extra_setup - true - ''; - finalize-script = - finalizeScript - # sh - '' - # push_command - true - ''; - image = mkImage [ - (setupScript - # sh - '' - cachedir="$(pwd)/.nix-cache" - echo "Configuring caching with the Runner Cache in $cachedir..." - export NIX_SUBSTITUTERS="$NIX_SUBSTITUTERS file://$cachedir?priority=10&trusted=true" - '') - (finalizeScript - # sh - '' - # add ^* to all store paths ending in .drv (prevent warning log spam) - ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "file://$(pwd)/.nix-cache" --stdin || true - '') - ]; - image-cachix = mkImage [ - (setupScript - # sh - '' - echo "Configuring caching with cachix..." - ${pkgs.cachix}/bin/cachix use $CACHIX_CACHE || true - '') - (finalizeScript - # sh - '' - ${pkgs.cachix}/bin/cachix push $CACHIX_CACHE || true - '') - ]; - image-attic = mkImage [ - (setupScript - # sh - '' - echo "Configuring caching with attic..." - ${pkgs.attic-client}/bin/attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true - ${pkgs.attic-client}/bin/attic use "$ATTIC_CACHE" || true - '') - (finalizeScript - # sh - '' - ${pkgs.attic-client}/bin/attic push --stdin ci:$ATTIC_CACHE || true - '') - ]; + setup-script = setupScript; + finalize-script = finalizeScript; + image = pkgs.dockerTools.buildImage { + name = "nix-ci"; + fromImage = pkgs.dockerTools.pullImage { + imageName = "nixpkgs/nix-flakes"; + # nix run nixpkgs#nix-prefetch-docker -- --image-name nixpkgs/nix-flakes --image-tag latest --arch --os linux + imageDigest = "sha256:95bce4317c15dfab3babac5a6d19d3ed41e31a02a8aaf3d4f6639778cb763b0a"; + sha256 = + if pkgs.stdenv.hostPlatform.isAarch64 + then "DMlSaP+ZVqxd9NxdFydGyfkuJdmOW5jt5iM/7cDyTEM=" + else "mfTNlGOpThanLlLQ2lL1RTcHqZJWdqUafYDZMeZPWEk="; + finalImageName = "nixpkgs/nix-flakes"; + finalImageTag = "latest"; + }; + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ + pkgs.gitMinimal + pkgs.gnugrep + pkgs.cachix + pkgs.attic-client + setupScript + finalizeScript + ]; + pathsToLink = ["/bin"]; + }; + }; }; checks = packages; @@ -206,11 +210,7 @@ # flake & devenv related flake-parts.url = "github:hercules-ci/flake-parts"; systems.url = "github:nix-systems/default-linux"; - devenv = { - url = "github:cachix/devenv"; - inputs.git-hooks.follows = "git-hooks"; - }; - git-hooks.url = "github:cachix/git-hooks.nix"; + devenv.url = "github:cachix/devenv"; treefmt-nix.url = "github:numtide/treefmt-nix"; }; diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 64f7926..5b9b86a 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -9,137 +9,150 @@ pkgs, ... }: let + inherit (lib) isAttrs filterAttrs mapAttrs types mkOption toList; cfg = config.ci.config; filterAttrsRec = pred: v: - if lib.isAttrs v - then lib.filterAttrs pred (lib.mapAttrs (path: filterAttrsRec pred) v) + if isAttrs v + then filterAttrs pred (mapAttrs (path: filterAttrsRec pred) v) else v; - subType = options: lib.types.submodule {inherit options;}; + subType = options: types.submodule {inherit options;}; mkNullOption = type: - lib.mkOption { + mkOption { default = null; - type = lib.types.nullOr type; + type = 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"; - }; + configType = subType { + 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"; }; - 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 { + }; + jobType = subType { + # nix ci opts + nix = mkOption { type = subType { - config = mkOption { - type = configType; - description = '' - Configuration options for the nix part itself - ''; - default = {}; + enable = mkOption { + type = types.bool; + default = cfg.nix-jobs-per-default; + description = "Handle this job as a nix job"; }; - 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 = {}; + deps = mkOption { + type = types.listOf types.package; + default = []; + description = "Dependencies/packages to install for this job"; + }; + enable-runner-cache = mkOption { + type = types.bool; + default = false; + description = '' + Cache this job using the GitLab Runner cache. + Warning: useful for tiny jobs, but most of the time it just takes an eternity. + ''; + }; + runner-cache-key = mkOption { + type = types.str; + default = "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"; + description = "Cache key to use for the runner nix cache. Requires enable-runner-cache = true"; }; }; + 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 = "$_NIX_CI_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); + }; + + ciType = 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 = {}; + }; + }; + in { + options = { + pipelines = mkOption { + type = types.lazyAttrsOf ciType; + description = '' + Create multiple GitLab CI pipelines. + + See README.md for more information about how a pipeline is selected. + ''; + default = {}; + apply = op: let + # NOTE: show warning if "default" is set and config.ci is not {} + legacyMode = config.ci != {}; + defaultExists = builtins.hasAttr "default" op; + value = + { + "default" = config.ci; + } + // op; + in + if defaultExists && legacyMode + then builtins.trace "Warning: config.ci is overwritten by pipelines.default" value + else value; + }; + ci = mkOption { + type = ciType; + description = '' + Note: this is a shorthand for writing `pipelines."default"` + 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. ''; @@ -175,94 +188,100 @@ } ) (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; + lib.fold (pipeline: acc: acc // pipeline) {} (map ( + pipeline_name: let + pipeline = config.pipelines."${pipeline_name}"; + jobs = filterAttrsRec (n: v: v != null) pipeline.jobs; + rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs pipeline ["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:pipeline:${pipeline_name}:job-deps:${key}"; + value = pkgs.writeShellScript "gitlab-ci-job-deps:${key}" '' + export PATH="${lib.makeBinPath job.nix.deps}:$PATH"; + # variables containing nix derivations: + ${variableExports} + ''; + }) + jobs; + # allows the user to directly run the script + jobsMappedForScript = + mapAttrs (key: job: let + variablesWithoutStorePaths = filterJobVariables false job; + variableExports = lib.concatLines ( + lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithoutStorePaths + ); + in { + name = "gitlab-ci:pipeline:${pipeline_name}:job:${key}"; + value = pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' + # set up deps and environment variables containing store paths + . ${jobsMappedForDeps."gitlab-ci:pipeline:${pipeline_name}: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 \"gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}\"" + ] + (appendToAfterScript [ + "finalize_nix_ci" + ] + job)) + // lib.optionalAttrs job.nix.enable { + image = job.image; + variables = + (filterJobVariables false job) + // lib.optionalAttrs job.nix.enable-runner-cache { + _NIX_CI_CACHE_STRATEGY = "runner"; + }; + cache = + ( + let + c = job.cache or []; + in + toList c + ) + ++ (lib.optional (job.nix.enable-runner-cache) { + key = job.nix.runner-cache-key; + paths = [".nix-cache/"]; + }); + } + ) ["nix"]; + }) + jobs; + in + # gitlab-ci:pipeline: + # gitlab-ci:pipeline::job: + # gitlab-ci:pipeline::job-deps: + { + "gitlab-ci:pipeline:${pipeline_name}" = toYaml "gitlab-ci-${pipeline_name}.yml" (rest // jobsPatched); + } + // jobsMappedForDeps + // jobsMappedForScript + ) (builtins.attrNames config.pipelines)); } ); } diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 911556a..dd1e79a 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -1,29 +1,37 @@ spec: inputs: - image_tag: + cache_strategy: type: string - description: "latest | latest-cachix | latest-attic etc." - default: latest + description: | + (empty for auto) | none | runner | cachix | attic + When left empty $NIX_CI_CACHE_STRATEGY will be used, which defaults to none + default: "" cache_files: type: array description: | Files to use as the cache key for the generated pipeline yaml. If you use "ci.nix" to define CI, add that here for example default: ["flake.nix", "flake.lock"] - disable_cache: - type: string - description: | - Disables any caching provided by this component. Set to any non-empty value to disable caching. - default: "" --- stages: - build - trigger variables: - NIX_CI_DISABLE_CACHE: "$[[ inputs.disable_cache ]]${NIX_CI_DISABLE_CACHE:-}" + # which version of the image should be used + _NIX_CI_VERSION: ${NIX_CI_VERSION} + _NIX_CI_IMAGE: ${NIX_CI_IMAGE:-registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:${_NIX_CI_VERSION}} + # force build the pipeline yaml + _NIX_CI_FORCE_BUILD: ${NIX_CI_FORCE_BUILD} + # disable caching on the child pipeline jobs + _NIX_CI_DISABLE_CACHE: ${NIX_CI_DISABLE_CACHE} + # type of cache strategy to use (none, runner, attic, cachix) + _CACHE_STRATEGY_TMP: $[[ inputs.cache_strategy ]] + _NIX_CI_CACHE_STRATEGY: ${NIX_CI_CACHE_STRATEGY:-${_CACHE_STRATEGY_TMP:-none}} + # for multiple pipelines + _NIX_CI_PIPELINE_NAME: ${NIX_CI_PIPELINE_NAME:-${CI_PIPELINE_SOURCE:-default}} nix-ci:build: stage: build - image: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$[[ inputs.image_tag ]] + image: $_NIX_CI_IMAGE cache: - key: files: $[[ inputs.cache_files ]] @@ -36,7 +44,7 @@ nix-ci:build: # generated-gitlab-ci.yml exists in the cache - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - - '[ -n "$NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' + - '[ -n "$_NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' # only setup when we need to generate the pipeline yaml - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' script: From 786abd917cb10c1c6d5d5327b5493d4dfb1f9e65 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 12:34:30 +0100 Subject: [PATCH 022/102] fix: use new format of specifying version --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84edac4..79c0db6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ include: - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA - inputs: - image_tag: $CI_COMMIT_SHORT_SHA +variables: + NIX_CI_VERSION: $CI_COMMIT_SHORT_SHA stages: - build-images - build From b0d24f753c1047cd68e97f05658e9aceabb83f1b Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 16:04:59 +0100 Subject: [PATCH 023/102] fix(image): fix digest being different for every arch aswell --- flake.nix | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index f07964e..7a27ea3 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,7 @@ pkgs, inputs', config, + system, ... }: rec { treefmt = { @@ -174,17 +175,25 @@ finalize-script = finalizeScript; image = pkgs.dockerTools.buildImage { name = "nix-ci"; - fromImage = pkgs.dockerTools.pullImage { - imageName = "nixpkgs/nix-flakes"; - # nix run nixpkgs#nix-prefetch-docker -- --image-name nixpkgs/nix-flakes --image-tag latest --arch --os linux - imageDigest = "sha256:95bce4317c15dfab3babac5a6d19d3ed41e31a02a8aaf3d4f6639778cb763b0a"; - sha256 = - if pkgs.stdenv.hostPlatform.isAarch64 - then "DMlSaP+ZVqxd9NxdFydGyfkuJdmOW5jt5iM/7cDyTEM=" - else "mfTNlGOpThanLlLQ2lL1RTcHqZJWdqUafYDZMeZPWEk="; - finalImageName = "nixpkgs/nix-flakes"; - finalImageTag = "latest"; - }; + fromImage = let + hashes = { + "aarch64-linux" = "sha256-mfTNlGOpThanLlLQ2lL1RTcHqZJWdqUafYDZMeZPWEk="; + "x86_64-linux" = "sha256-DMlSaP+ZVqxd9NxdFydGyfkuJdmOW5jt5iM/7cDyTEM="; + }; + # skopeo inspect --raw docker://nixpkgs/nix-flakes + digests = { + "aarch64-linux" = "sha256:5113a4d10dda16c30bf2e517f29a56890233d2660115003155aab1f7d279d8db"; + "x86_64-linux" = "sha256:be07ecf4b5c19be83f63b6f5c7f21bcaf19cf722d339c99cfe2b2ad09f81a7fa"; + }; + sha256 = hashes.${system} or (throw "Unsupported system"); + imageDigest = digests.${system} or (throw "Unsupported system"); + in + pkgs.dockerTools.pullImage { + imageName = "nixpkgs/nix-flakes"; + inherit sha256 imageDigest; + finalImageName = "nixpkgs/nix-flakes"; + finalImageTag = "latest"; + }; copyToRoot = pkgs.buildEnv { name = "image-root"; paths = [ From 9d289842615d2c19ade60db0558a2f2ce0a81255 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 16:21:11 +0100 Subject: [PATCH 024/102] fix(ci): install buildah in the correct job --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 79c0db6..08e6d0c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,9 +12,6 @@ build:image: matrix: - ARCH: ["x86_64-linux", "aarch64-linux"] image: nixpkgs/nix-flakes:latest - before_script: - - nix profile install nixpkgs#buildah - - export PATH="$PATH:$HOME/.nix-profile/bin" script: - nix build .#image --system $ARCH after_script: @@ -27,6 +24,8 @@ deploy:image: needs: - build:image before_script: + - nix profile install nixpkgs#buildah + - export PATH="$PATH:$HOME/.nix-profile/bin" - export REGISTRY_AUTH_FILE=''${HOME}/auth.json - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY - mkdir -p /etc/containers && echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json From b4071d41715824093d09fe0948765e56fdc37616 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 16:36:50 +0100 Subject: [PATCH 025/102] fix(ci): image seems quite important ngl --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08e6d0c..9542bde 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,6 +21,7 @@ build:image: - dist deploy:image: stage: build-images + image: nixpkgs/nix-flakes:latest needs: - build:image before_script: From 7569fcd0863589520ecbef48f0f8f65470e30776 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 18:24:23 +0100 Subject: [PATCH 026/102] fix(ci): work around gitlab's variable substitution limitations --- flake.nix | 2 +- templates/nix-gitlab-ci.yml | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index 7a27ea3..71ee476 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,6 @@ flake = {}; perSystem = { pkgs, - inputs', config, system, ... @@ -26,6 +25,7 @@ mdformat.enable = true; yamlfmt.enable = true; }; + settings.formatter.yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; }; devenv.shells.default = { containers = pkgs.lib.mkForce {}; diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index dd1e79a..6d45a96 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -4,8 +4,8 @@ spec: type: string description: | (empty for auto) | none | runner | cachix | attic - When left empty $NIX_CI_CACHE_STRATEGY will be used, which defaults to none - default: "" + Sets the default strategy and will be overridden by $NIX_CI_CACHE_STRATEGY + default: "none" cache_files: type: array description: | @@ -19,17 +19,29 @@ stages: variables: # which version of the image should be used _NIX_CI_VERSION: ${NIX_CI_VERSION} - _NIX_CI_IMAGE: ${NIX_CI_IMAGE:-registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:${_NIX_CI_VERSION}} + _NIX_CI_IMAGE: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:${_NIX_CI_VERSION} # force build the pipeline yaml _NIX_CI_FORCE_BUILD: ${NIX_CI_FORCE_BUILD} # disable caching on the child pipeline jobs _NIX_CI_DISABLE_CACHE: ${NIX_CI_DISABLE_CACHE} # type of cache strategy to use (none, runner, attic, cachix) - _CACHE_STRATEGY_TMP: $[[ inputs.cache_strategy ]] - _NIX_CI_CACHE_STRATEGY: ${NIX_CI_CACHE_STRATEGY:-${_CACHE_STRATEGY_TMP:-none}} + _NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] # for multiple pipelines - _NIX_CI_PIPELINE_NAME: ${NIX_CI_PIPELINE_NAME:-${CI_PIPELINE_SOURCE:-default}} + _NIX_CI_PIPELINE_NAME: ${CI_PIPELINE_SOURCE} +.env_overrides: &env_overrides + rules: + - if: $NIX_CI_IMAGE != null + variables: + _NIX_CI_IMAGE: $NIX_CI_IMAGE + - if: $NIX_CI_CACHE_STRATEGY != null + variables: + _NIX_CI_CACHE_STRATEGY: $NIX_CI_CACHE_STRATEGY + - if: $NIX_CI_PIPELINE_NAME != null + variables: + _NIX_CI_PIPELINE_NAME: $NIX_CI_PIPELINE_NAME + - when: on_success nix-ci:build: + <<: *env_overrides stage: build image: $_NIX_CI_IMAGE cache: @@ -60,6 +72,7 @@ nix-ci:build: paths: - generated-gitlab-ci.yml nix-ci:trigger: + <<: *env_overrides stage: trigger needs: - nix-ci:build From 244142274daf54cc61b58822b5187da1d07c762e Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 21 Feb 2025 18:52:55 +0100 Subject: [PATCH 027/102] fix(ci): build correct package and hopefully handle cache issues --- templates/nix-gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 6d45a96..a3c83e8 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -53,15 +53,15 @@ nix-ci:build: paths: - .nix-cache/ before_script: - # generated-gitlab-ci.yml exists in the cache - - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' + # generated-gitlab-ci-.yml exists in the cache + - '[ -f "generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - '[ -n "$_NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' # only setup when we need to generate the pipeline yaml - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' script: # build the generated-gitlab-ci.yml if it does not exist in the cache - - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci-config && install result generated-gitlab-ci.yml; fi' + - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME} && install result generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml; fi' after_script: # NOTE: environment variables of before_script and script don't exist here anymore # From ebc70d5c18b7f0f7e02845582f1cd177f6e95dc6 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Fri, 21 Feb 2025 21:26:28 +0000 Subject: [PATCH 028/102] fix(ci): fix artifact path --- templates/nix-gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index a3c83e8..d45730c 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -70,7 +70,7 @@ nix-ci:build: - 'if [ -f "/tmp/nix-store-before" ]; then finalize_nix_ci; fi' artifacts: paths: - - generated-gitlab-ci.yml + - generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml nix-ci:trigger: <<: *env_overrides stage: trigger From 55f90b4261827ea0be98819ca08d9586fee8fee0 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Fri, 21 Feb 2025 22:53:00 +0000 Subject: [PATCH 029/102] fix(ci): update artifact name in trigger job aswell --- templates/nix-gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index d45730c..ad2eb0f 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -78,7 +78,7 @@ nix-ci:trigger: - nix-ci:build trigger: include: - - artifact: generated-gitlab-ci.yml + - artifact: generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml job: nix-ci:build strategy: depend forward: From 85431f78aa88dfab9bc3d179c5c753a5ec60ce11 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 22 Feb 2025 21:57:25 +0100 Subject: [PATCH 030/102] chore: improve "default" handling & caching add new env variables to configure which CI source gets mapped to default add logic to check if pipeline exists in flake, else build "default" --- templates/nix-gitlab-ci.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index ad2eb0f..705d9aa 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -28,6 +28,10 @@ variables: _NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] # for multiple pipelines _NIX_CI_PIPELINE_NAME: ${CI_PIPELINE_SOURCE} + # set NIX_CI_DEFAULT_SOURCES to something like "/^web/" or "/^web|push/", + # then this will default to the "default" pipeline for these sources. + # By default we map everything to "default" (man that's quite a bunch of defaults) + _NIX_CI_DEFAULT_SOURCES: "/.*/" .env_overrides: &env_overrides rules: - if: $NIX_CI_IMAGE != null @@ -36,6 +40,12 @@ variables: - if: $NIX_CI_CACHE_STRATEGY != null variables: _NIX_CI_CACHE_STRATEGY: $NIX_CI_CACHE_STRATEGY + - if: $NIX_CI_DEFAULT_SOURCES != null + variables: + _NIX_CI_DEFAULT_SOURCES: $NIX_CI_DEFAULT_SOURCES + - if: '$CI_PIPELINE_SOURCE =~ $_NIX_CI_DEFAULT_SOURCES' + variables: + _NIX_CI_PIPELINE_NAME: default - if: $NIX_CI_PIPELINE_NAME != null variables: _NIX_CI_PIPELINE_NAME: $NIX_CI_PIPELINE_NAME @@ -47,30 +57,30 @@ nix-ci:build: cache: - key: files: $[[ inputs.cache_files ]] + prefix: $_NIX_CI_PIPELINE_NAME paths: - generated-gitlab-ci.yml - key: nix paths: - .nix-cache/ before_script: - # generated-gitlab-ci-.yml exists in the cache - - '[ -f "generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' + # generated-gitlab-ci.yml exists in the cache + - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - '[ -n "$_NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' # only setup when we need to generate the pipeline yaml - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' script: # build the generated-gitlab-ci.yml if it does not exist in the cache - - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME} && install result generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml; fi' + - 'if [ -z "$CACHED" ]; then nix eval --json ".#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME}" &>/dev/null && _NAME=$_NIX_CI_PIPELINE_NAME || _NAME="default"; fi' + - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NAME} && install result generated-gitlab-ci.yml; fi' after_script: - # NOTE: environment variables of before_script and script don't exist here anymore - # # save to binary cache or Gitlab CI cache only if we actually built something # check if /tmp/nix-store-before exists as $CACHED never exists here and the file only exists if "setup_nix_ci" is called - 'if [ -f "/tmp/nix-store-before" ]; then finalize_nix_ci; fi' artifacts: paths: - - generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml + - generated-gitlab-ci.yml nix-ci:trigger: <<: *env_overrides stage: trigger @@ -78,7 +88,7 @@ nix-ci:trigger: - nix-ci:build trigger: include: - - artifact: generated-gitlab-ci-${_NIX_CI_PIPELINE_NAME}.yml + - artifact: generated-gitlab-ci.yml job: nix-ci:build strategy: depend forward: From fa6c098e0227a79accdf550d7642781603d476b3 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 27 Feb 2025 10:52:47 +0100 Subject: [PATCH 031/102] chore: improve nix-ci:build build time by using only builtins.toJSON removes dependency on remarshal, pkgs.formats.json uses jq to pretty print so we just use builtins.toJSON directly --- lib/flakeModule.nix | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 5b9b86a..72daa78 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -161,7 +161,15 @@ }; config.legacyPackages = let - toYaml = (pkgs.formats.yaml {}).generate; + # NOTE: json is also valid yaml and this removes dependency on jq + # and/or remarshal (used in pkgs.formats.json and pkgs.formats.yaml + # respectively) + toYaml = name: value: + pkgs.runCommand name {} '' + cat <> $out + ${builtins.toJSON value} + EOT + ''; mapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); prepend = key: arr: job: job From 36c4fc0fd6433b833e08f49bd32d2d45e591efd2 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 27 Feb 2025 11:19:50 +0100 Subject: [PATCH 032/102] fix(module): prevent parameter expansion when generating pipeline config --- lib/flakeModule.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 72daa78..7c34aa7 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -166,7 +166,7 @@ # respectively) toYaml = name: value: pkgs.runCommand name {} '' - cat <> $out + cat <<'EOT' > $out ${builtins.toJSON value} EOT ''; From 7a40a68a10fe936847b414093f350e826342ce2d Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 28 Feb 2025 11:54:41 +0100 Subject: [PATCH 033/102] chore(template): skip checking if pipeline exists, just fail if not --- templates/nix-gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 705d9aa..51b9670 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -72,8 +72,7 @@ nix-ci:build: - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' script: # build the generated-gitlab-ci.yml if it does not exist in the cache - - 'if [ -z "$CACHED" ]; then nix eval --json ".#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME}" &>/dev/null && _NAME=$_NIX_CI_PIPELINE_NAME || _NAME="default"; fi' - - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NAME} && install result generated-gitlab-ci.yml; fi' + - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME} && install result generated-gitlab-ci.yml; fi' after_script: # save to binary cache or Gitlab CI cache only if we actually built something # check if /tmp/nix-store-before exists as $CACHED never exists here and the file only exists if "setup_nix_ci" is called From 060d8fad47b51fcaa2319d15279c0b7ef99e7e31 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 28 Feb 2025 12:53:17 +0100 Subject: [PATCH 034/102] chore(module): slim down pipeline yaml generation even more --- lib/flakeModule.nix | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 7c34aa7..7e311de 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -164,12 +164,7 @@ # NOTE: json is also valid yaml and this removes dependency on jq # and/or remarshal (used in pkgs.formats.json and pkgs.formats.yaml # respectively) - toYaml = name: value: - pkgs.runCommand name {} '' - cat <<'EOT' > $out - ${builtins.toJSON value} - EOT - ''; + toYaml = name: value: builtins.toFile name (builtins.toJSON value); mapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); prepend = key: arr: job: job From a63376edf15eeeca477afbbdea429f9a01ef4a58 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 4 Mar 2025 14:29:22 +0100 Subject: [PATCH 035/102] chore: rename env variables back to non-_ versions for testing, CI template still needs adjustments --- flake.nix | 16 ++++++++-------- lib/flakeModule.nix | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index 71ee476..7047d00 100644 --- a/flake.nix +++ b/flake.nix @@ -92,9 +92,9 @@ echo -e "\\e[0Ksection_start:`date +%s`:nix_setup\\r\\e[0KSetting up Nix CI" nix path-info --all > /tmp/nix-store-before - if [ -z "$_NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($_NIX_CI_CACHE_STRATEGY)" - case "$_NIX_CI_CACHE_STRATEGY" in + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" + case "$NIX_CI_CACHE_STRATEGY" in "runner") export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} echo "Runner Cache: $RUNNER_CACHE" @@ -116,7 +116,7 @@ echo "Cache strategy is none, doing nothing..." ;; *) - echo "WARNING: Invalid cache strategy set: '$_NIX_CI_CACHE_STRATEGY'" + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" ;; esac echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" @@ -141,10 +141,10 @@ --old-line-format="" --unchanged-line-format="" \ /tmp/nix-store-before /tmp/nix-store-after) COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") - if [ -z "$_NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($_NIX_CI_CACHE_STRATEGY)" + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" echo $NEW_PATHS | { - case "$_NIX_CI_CACHE_STRATEGY" in + case "$NIX_CI_CACHE_STRATEGY" in "runner") export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} # add ^* to all store paths ending in .drv (prevent warning log spam) @@ -160,7 +160,7 @@ echo "Cache strategy is none, doing nothing..." ;; *) - echo "WARNING: Invalid cache strategy set: '$_NIX_CI_CACHE_STRATEGY'" + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" ;; esac } diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 7e311de..8dbf97e 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -25,7 +25,7 @@ }; configType = subType { - nix-jobs-per-default = mkOption { + nix-jobs-by-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"; @@ -73,7 +73,7 @@ }; image = mkOption { type = types.str; - default = "$_NIX_CI_IMAGE"; + default = "$NIX_CI_IMAGE"; }; after_script = mkNullOption (types.listOf types.str); allow_failure = mkNullOption (types.either types.attrs types.bool); @@ -258,7 +258,7 @@ variables = (filterJobVariables false job) // lib.optionalAttrs job.nix.enable-runner-cache { - _NIX_CI_CACHE_STRATEGY = "runner"; + NIX_CI_CACHE_STRATEGY = "runner"; }; cache = ( From dab96d0acdc8707208f632878fae766e16e9cbdf Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 4 Mar 2025 15:23:46 +0100 Subject: [PATCH 036/102] fix(module): rename variable --- lib/flakeModule.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 8dbf97e..c2ba9f9 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -37,7 +37,7 @@ type = subType { enable = mkOption { type = types.bool; - default = cfg.nix-jobs-per-default; + default = cfg.nix-jobs-by-default; description = "Handle this job as a nix job"; }; deps = mkOption { From a5fba6d27d7ccfc8e159f51f4412843cae14fe44 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 4 Mar 2025 16:05:33 +0100 Subject: [PATCH 037/102] chore(flake): collapse nix ci top level section again for less clutter --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 7047d00..b02a85d 100644 --- a/flake.nix +++ b/flake.nix @@ -89,7 +89,7 @@ packages = let setupScript = pkgs.writeShellScriptBin "setup_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:nix_setup\\r\\e[0KSetting up Nix CI" + echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" nix path-info --all > /tmp/nix-store-before if [ -z "$NIX_CI_DISABLE_CACHE" ]; then @@ -134,7 +134,7 @@ echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" ''; finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci\\r\\e[0KFinalizing Nix CI..." + echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." nix path-info --all > /tmp/nix-store-after echo "Finding new paths..." NEW_PATHS=$(${pkgs.diffutils}/bin/diff --new-line-format="%L" \ From f1b8b5a210a71ce40a3abdf5f3ca0db0fd59ee97 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 4 Mar 2025 16:12:51 +0100 Subject: [PATCH 038/102] refactor(template): get rid of rules and work around limitations in a new way --- .gitlab-ci.yml | 4 +- templates/nix-gitlab-ci.yml | 98 ++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9542bde..24b9507 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ include: - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA -variables: - NIX_CI_VERSION: $CI_COMMIT_SHORT_SHA + inputs: + version: $CI_COMMIT_SHORT_SHA stages: - build-images - build diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 51b9670..6ac8a82 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -10,84 +10,82 @@ spec: type: array description: | Files to use as the cache key for the generated pipeline yaml. - If you use "ci.nix" to define CI, add that here for example + If you use "ci.nix" to define CI, add that here for example. + Note that max 2 items are allowed in cache:key:files, so use something like + ["flake.*", "ci.nix"] f. ex. to match flake.lock, flake.nix and ci.nix. default: ["flake.nix", "flake.lock"] + version: + type: string + description: | + Which version of the Nix CI image to use. Using a tag/version is recommended. --- stages: - build - trigger variables: - # which version of the image should be used - _NIX_CI_VERSION: ${NIX_CI_VERSION} - _NIX_CI_IMAGE: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:${_NIX_CI_VERSION} - # force build the pipeline yaml - _NIX_CI_FORCE_BUILD: ${NIX_CI_FORCE_BUILD} - # disable caching on the child pipeline jobs - _NIX_CI_DISABLE_CACHE: ${NIX_CI_DISABLE_CACHE} - # type of cache strategy to use (none, runner, attic, cachix) - _NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] - # for multiple pipelines - _NIX_CI_PIPELINE_NAME: ${CI_PIPELINE_SOURCE} - # set NIX_CI_DEFAULT_SOURCES to something like "/^web/" or "/^web|push/", - # then this will default to the "default" pipeline for these sources. - # By default we map everything to "default" (man that's quite a bunch of defaults) - _NIX_CI_DEFAULT_SOURCES: "/.*/" -.env_overrides: &env_overrides - rules: - - if: $NIX_CI_IMAGE != null - variables: - _NIX_CI_IMAGE: $NIX_CI_IMAGE - - if: $NIX_CI_CACHE_STRATEGY != null - variables: - _NIX_CI_CACHE_STRATEGY: $NIX_CI_CACHE_STRATEGY - - if: $NIX_CI_DEFAULT_SOURCES != null - variables: - _NIX_CI_DEFAULT_SOURCES: $NIX_CI_DEFAULT_SOURCES - - if: '$CI_PIPELINE_SOURCE =~ $_NIX_CI_DEFAULT_SOURCES' - variables: - _NIX_CI_PIPELINE_NAME: default - - if: $NIX_CI_PIPELINE_NAME != null - variables: - _NIX_CI_PIPELINE_NAME: $NIX_CI_PIPELINE_NAME - - when: on_success + # These can be overriden, see https://docs.gitlab.com/ci/variables/#cicd-variable-precedence + # which image should be used by default. + NIX_CI_IMAGE: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$[[ inputs.version ]] + # default cache stategy + NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] nix-ci:build: - <<: *env_overrides stage: build - image: $_NIX_CI_IMAGE + image: $NIX_CI_IMAGE cache: - key: files: $[[ inputs.cache_files ]] - prefix: $_NIX_CI_PIPELINE_NAME paths: - - generated-gitlab-ci.yml + - .nix-ci-pipelines/ - key: nix paths: - .nix-cache/ before_script: - # generated-gitlab-ci.yml exists in the cache - - '[ -f "generated-gitlab-ci.yml" ] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true' - # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - - '[ -n "$_NIX_CI_FORCE_BUILD" ] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true' - # only setup when we need to generate the pipeline yaml - - 'if [ -z "$CACHED" ]; then source setup_nix_ci; fi' + - | + # if no explicit pipeline is requested + if [[ -z "${NIX_CI_PIPELINE_NAME:-}" ]]; then + # if regex matches, use pipeline "default", otherwise $CI_PIPELINE_SOURCE + [[ "${CI_PIPELINE_SOURCE}" =~ ${NIX_CI_DEFAULT_SOURCES:-.*} ]] \ + && NIX_CI_PIPELINE_NAME="default" \ + || NIX_CI_PIPELINE_NAME="$CI_PIPELINE_SOURCE"; + fi + echo "NIX_CI_GENERATED_PIPELINE_NAME=$NIX_CI_PIPELINE_NAME" >> trigger.env + + mkdir -p .nix-ci-pipelines/ + # generated-gitlab-ci.yml exists in the cache + [[ -f ".nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml" ]] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true + # allow the user to manually skip the cache (when the key files are not correctly configured etc.) + [[ -n "$NIX_CI_FORCE_BUILD" ]] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true + + # only setup when we need to generate the pipeline yaml + if [[ -z "$CACHED" ]]; then + source setup_nix_ci; + fi script: - # build the generated-gitlab-ci.yml if it does not exist in the cache - - 'if [ -z "$CACHED" ]; then nix build .#gitlab-ci:pipeline:${_NIX_CI_PIPELINE_NAME} && install result generated-gitlab-ci.yml; fi' + # build the pipeline if it does not exist in the cache + - > + if [[ -z "$CACHED" ]]; then + nix build .#gitlab-ci:pipeline:${NIX_CI_PIPELINE_NAME} && install result .nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml; + fi after_script: # save to binary cache or Gitlab CI cache only if we actually built something # check if /tmp/nix-store-before exists as $CACHED never exists here and the file only exists if "setup_nix_ci" is called - - 'if [ -f "/tmp/nix-store-before" ]; then finalize_nix_ci; fi' + - | + if [[ -f "/tmp/nix-store-before" ]]; then + finalize_nix_ci; + fi artifacts: paths: - - generated-gitlab-ci.yml + - .nix-ci-pipelines/ + reports: + dotenv: trigger.env + nix-ci:trigger: - <<: *env_overrides stage: trigger needs: - nix-ci:build trigger: include: - - artifact: generated-gitlab-ci.yml + - artifact: .nix-ci-pipelines/${NIX_CI_GENERATED_PIPELINE_NAME}.yml job: nix-ci:build strategy: depend forward: From 6fa28062191beeb36ba305ef53086ed1c39ebc3e Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 4 Mar 2025 16:23:51 +0100 Subject: [PATCH 039/102] docs(readme): update usage infos --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da2b36b..61eac0c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Also makes it possible to split CI parts in a separate module which can be impor # flake.nix { ... - inputs.nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci?dir=lib"; + inputs.nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; # recommendation: pin to the latest release/version outputs = {...}: flake-parts.lib.mkFlake {...} { imports = [ @@ -48,7 +48,9 @@ Also makes it possible to split CI parts in a separate module which can be impor ```yaml # .gitlab-ci.yml include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: use the latest version (try not to use latest) + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: pin to the latest release/version (don't use "main" etc.) + inputs: + version: # docker image tag, use the same version as a above ``` ## Utilities From 62e465c09488fbe0b845b769f986e37d1b8342da Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 7 Mar 2025 17:32:49 +0100 Subject: [PATCH 040/102] fix(template): overriding default variables in child pipeline --- templates/nix-gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 6ac8a82..b00ac79 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -49,6 +49,13 @@ nix-ci:build: || NIX_CI_PIPELINE_NAME="$CI_PIPELINE_SOURCE"; fi echo "NIX_CI_GENERATED_PIPELINE_NAME=$NIX_CI_PIPELINE_NAME" >> trigger.env + # inheritance of pipeline variables is a bit weird, so explicitly override them + # (ctx: setting any of these in the project variables would only apply correctly + # in this pipeline, not the child pipeline, instead weirdly enough the default + # variables above are used). If any other variables are added at the top, add them + # here aswell + echo "NIX_CI_IMAGE=$NIX_CI_IMAGE" >> trigger.env + echo "NIX_CI_CACHE_STRATEGY=$NIX_CI_CACHE_STRATEGY" >> trigger.env mkdir -p .nix-ci-pipelines/ # generated-gitlab-ci.yml exists in the cache From 1e978a3edf61b224a958a9ad1d25edf7d3f2c0ca Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 22 Mar 2025 21:48:54 +0100 Subject: [PATCH 041/102] refactor(flake): move setup and finalize script to separate files --- flake.nix | 84 +------------------------------------- scripts/finalize_nix_ci.sh | 37 +++++++++++++++++ scripts/setup_nix_ci.sh | 44 ++++++++++++++++++++ 3 files changed, 83 insertions(+), 82 deletions(-) create mode 100644 scripts/finalize_nix_ci.sh create mode 100644 scripts/setup_nix_ci.sh diff --git a/flake.nix b/flake.nix index b02a85d..8c5560b 100644 --- a/flake.nix +++ b/flake.nix @@ -88,88 +88,8 @@ }; packages = let - setupScript = pkgs.writeShellScriptBin "setup_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" - nix path-info --all > /tmp/nix-store-before - - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" - case "$NIX_CI_CACHE_STRATEGY" in - "runner") - export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} - echo "Runner Cache: $RUNNER_CACHE" - export NIX_CONFIG="$NIX_CONFIG - extra-trusted-substituters = $RUNNER_CACHE?priority=10&trusted=true - extra-substituters = $RUNNER_CACHE?priority=10&trusted=true - " - ;; - "attic") - echo "Attic Cache: $ATTIC_CACHE" - attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true - attic use "$ATTIC_CACHE" || true - ;; - "cachix") - echo "Cachix Cache: $CACHIX_CACHE" - cachix use "$CACHIX_CACHE" || true - ;; - "none") - echo "Cache strategy is none, doing nothing..." - ;; - *) - echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" - ;; - esac - echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" - else - echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration..." - fi - - # load the job's deps only if the name was passed - if [[ ! -z $1 ]]; then - echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching Nix dependencies for job" - nix build .#$1 - source $(readlink -f result) - echo -e "\\e[0Ksection_end:`date +%s`:nix_deps\\r\\e[0K" - fi - echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" - ''; - finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" '' - echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." - nix path-info --all > /tmp/nix-store-after - echo "Finding new paths..." - NEW_PATHS=$(${pkgs.diffutils}/bin/diff --new-line-format="%L" \ - --old-line-format="" --unchanged-line-format="" \ - /tmp/nix-store-before /tmp/nix-store-after) - COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" - echo $NEW_PATHS | { - case "$NIX_CI_CACHE_STRATEGY" in - "runner") - export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} - # add ^* to all store paths ending in .drv (prevent warning log spam) - ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true - ;; - "attic") - attic push --stdin ci:$ATTIC_CACHE || true - ;; - "cachix") - cachix push $CACHIX_CACHE || true - ;; - "none") - echo "Cache strategy is none, doing nothing..." - ;; - *) - echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" - ;; - esac - } - echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" - else - echo "Caching disabled, not uploading $COUNT new store entries..." - fi - echo -e "\\e[0Ksection_end:`date +%s`:finalize_nix_ci\\r\\e[0K" - ''; + setupScript = pkgs.writeShellScriptBin "setup_nix_ci" (builtins.readFile ./scripts/setup_nix_ci.sh); + finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" (builtins.readFile ./scripts/finalize_nix_ci.sh); in { setup-script = setupScript; finalize-script = finalizeScript; diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh new file mode 100644 index 0000000..e1200c2 --- /dev/null +++ b/scripts/finalize_nix_ci.sh @@ -0,0 +1,37 @@ +echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." + nix path-info --all > /tmp/nix-store-after + echo "Finding new paths..." + NEW_PATHS=$(${pkgs.diffutils}/bin/diff --new-line-format="%L" \ + --old-line-format="" --unchanged-line-format="" \ + /tmp/nix-store-before /tmp/nix-store-after) + COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") + + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" + echo $NEW_PATHS | { + case "$NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + # add ^* to all store paths ending in .drv (prevent warning log spam) + ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true + ;; + "attic") + attic push --stdin ci:$ATTIC_CACHE || true + ;; + "cachix") + cachix push $CACHIX_CACHE || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" + ;; + esac + } + echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" + else + echo "Caching disabled, not uploading $COUNT new store entries..." + fi +echo -e "\\e[0Ksection_end:`date +%s`:finalize_nix_ci\\r\\e[0K" + diff --git a/scripts/setup_nix_ci.sh b/scripts/setup_nix_ci.sh new file mode 100644 index 0000000..d91e17d --- /dev/null +++ b/scripts/setup_nix_ci.sh @@ -0,0 +1,44 @@ +echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" + nix path-info --all > /tmp/nix-store-before + + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" + case "$NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + echo "Runner Cache: $RUNNER_CACHE" + export NIX_CONFIG="$NIX_CONFIG + extra-trusted-substituters = $RUNNER_CACHE?priority=10&trusted=true + extra-substituters = $RUNNER_CACHE?priority=10&trusted=true + " + ;; + "attic") + echo "Attic Cache: $ATTIC_CACHE" + attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true + attic use "$ATTIC_CACHE" || true + ;; + "cachix") + echo "Cachix Cache: $CACHIX_CACHE" + cachix use "$CACHIX_CACHE" || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" + ;; + esac + echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" + else + echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration..." + fi + + # load the job's deps only if the name was passed + if [[ ! -z $1 ]]; then + echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching Nix dependencies for job" + nix build .#$1 + source $(readlink -f result) + echo -e "\\e[0Ksection_end:`date +%s`:nix_deps\\r\\e[0K" + fi +echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" + From d73485322345297b321fcfd02c618b88c1f2d85e Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 22 Mar 2025 21:50:33 +0100 Subject: [PATCH 042/102] chore(scripts): improve cache strategy handling, see #21 --- scripts/finalize_nix_ci.sh | 4 ++++ scripts/setup_nix_ci.sh | 5 +++++ templates/nix-gitlab-ci.yml | 11 ++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh index e1200c2..698b81f 100644 --- a/scripts/finalize_nix_ci.sh +++ b/scripts/finalize_nix_ci.sh @@ -6,6 +6,10 @@ echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0K /tmp/nix-store-before /tmp/nix-store-after) COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") + if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then + export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; + fi + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" echo $NEW_PATHS | { diff --git a/scripts/setup_nix_ci.sh b/scripts/setup_nix_ci.sh index d91e17d..1f2af66 100644 --- a/scripts/setup_nix_ci.sh +++ b/scripts/setup_nix_ci.sh @@ -1,6 +1,11 @@ echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" nix path-info --all > /tmp/nix-store-before + if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then + export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; + echo "NIX_CI_CACHE_STRATEGY was set to auto, selected '$NIX_CI_CACHE_STRATEGY' for this job" + fi + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" case "$NIX_CI_CACHE_STRATEGY" in diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index b00ac79..cc87788 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -3,9 +3,14 @@ spec: cache_strategy: type: string description: | - (empty for auto) | none | runner | cachix | attic - Sets the default strategy and will be overridden by $NIX_CI_CACHE_STRATEGY - default: "none" + auto (default) | none | runner | cachix | attic + Sets the default caching strategy. + - "auto": dynamically selects the best strategy for every job based on env variables + - "none": disables caching + - "runner", "cachix" & "attic": forces every job to use this strategy + + Can be overridden by setting NIX_CI_CACHE_STRATEGY in the pipeline variables. + default: "auto" cache_files: type: array description: | From 2bed4190a4c3fd4fb3a76ba66b6e18019893369a Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 23 Mar 2025 14:36:05 +0100 Subject: [PATCH 043/102] chore!: remove deprecated template --- gitlab-ci.yml | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 gitlab-ci.yml diff --git a/gitlab-ci.yml b/gitlab-ci.yml deleted file mode 100644 index f1cb469..0000000 --- a/gitlab-ci.yml +++ /dev/null @@ -1,33 +0,0 @@ -# -# NOTE: DEPRECATED: please switch to the CI/CD Component or include "templates/nix-gitlab-ci.yml" -# -variables: - # latest | latest-cachix | latest-attic etc. - NIX_CI_IMAGE_TAG: latest -stages: - - build - - trigger -nix-ci:build: - stage: build - image: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:${NIX_CI_IMAGE_TAG} - before_script: - - source setup_nix_ci - script: - # build the generated-gitlab-ci.yml - - nix build .#gitlab-ci-config - - install result generated-gitlab-ci.yml - after_script: - # upload to binary cache - - finalize_nix_ci - artifacts: - paths: - - generated-gitlab-ci.yml -nix-ci:trigger: - stage: trigger - needs: - - nix-ci:build - trigger: - include: - - artifact: generated-gitlab-ci.yml - job: nix-ci:build - strategy: depend From a458a21c7bfacd68f63fa85a19bfc4c8dbbcd20e Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 23 Mar 2025 14:55:40 +0100 Subject: [PATCH 044/102] fix(scripts): remove nix substitutions from shell script --- flake.nix | 22 ++++++++++++++-------- scripts/finalize_nix_ci.sh | 6 +++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index 8c5560b..53fb5ca 100644 --- a/flake.nix +++ b/flake.nix @@ -116,14 +116,20 @@ }; copyToRoot = pkgs.buildEnv { name = "image-root"; - paths = [ - pkgs.gitMinimal - pkgs.gnugrep - pkgs.cachix - pkgs.attic-client - setupScript - finalizeScript - ]; + paths = with pkgs; + [ + gitMinimal + gnugrep + gnused + busybox + diffutils + cachix + attic-client + ] + ++ [ + setupScript + finalizeScript + ]; pathsToLink = ["/bin"]; }; }; diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh index 698b81f..0c47039 100644 --- a/scripts/finalize_nix_ci.sh +++ b/scripts/finalize_nix_ci.sh @@ -1,10 +1,10 @@ echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." nix path-info --all > /tmp/nix-store-after echo "Finding new paths..." - NEW_PATHS=$(${pkgs.diffutils}/bin/diff --new-line-format="%L" \ + NEW_PATHS=$(diff --new-line-format="%L" \ --old-line-format="" --unchanged-line-format="" \ /tmp/nix-store-before /tmp/nix-store-after) - COUNT=$(${pkgs.busybox}/bin/wc -l <<<"$NEW_PATHS") + COUNT=$(wc -l <<<"$NEW_PATHS") if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; @@ -17,7 +17,7 @@ echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0K "runner") export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} # add ^* to all store paths ending in .drv (prevent warning log spam) - ${pkgs.gnused}/bin/sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true + sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true ;; "attic") attic push --stdin ci:$ATTIC_CACHE || true From 14530f8e9edc9cadc097e3b1f5188f6db0d28320 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Thu, 3 Apr 2025 15:40:36 +0200 Subject: [PATCH 045/102] fix: update flake inputs to be able to fix docker image archs being swapped --- flake.lock | 81 ++++++++++++++++++++++++++++-------------------------- flake.nix | 16 +++++------ 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/flake.lock b/flake.lock index ff34ba8..92c0748 100644 --- a/flake.lock +++ b/flake.lock @@ -14,11 +14,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1728672398, - "narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=", + "lastModified": 1737621947, + "narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=", "owner": "cachix", "repo": "cachix", - "rev": "aac51f698309fd0f381149214b7eee213c66ef0a", + "rev": "f65a3cd5e339c223471e64c051434616e18cc4f5", "type": "github" }, "original": { @@ -37,11 +37,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1732585607, - "narHash": "sha256-6ffeaSMuaL326f7KrCeScpSJtdHsFKS9gPrsSZkndvU=", + "lastModified": 1743687534, + "narHash": "sha256-QIh5jKWE0aZ4N77Zu3kz0RjA22zqqy5p6PfJnOW5rPE=", "owner": "cachix", "repo": "devenv", - "rev": "a520f05c40ebecaf5e17064b27e28ba8e70c49fb", + "rev": "efd5d68a6483573410565d0b940e1a67b6f92591", "type": "github" }, "original": { @@ -53,11 +53,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -93,11 +93,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1730504689, - "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "506278e768c2a08bec68eb62932193e341f55c90", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { @@ -118,11 +118,11 @@ ] }, "locked": { - "lastModified": 1737465171, - "narHash": "sha256-R10v2hoJRLq8jcL4syVFag7nIGE7m13qO48wRIukWNg=", + "lastModified": 1740849354, + "narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17", + "rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07", "type": "github" }, "original": { @@ -188,11 +188,11 @@ ] }, "locked": { - "lastModified": 1727438425, - "narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=", + "lastModified": 1741798497, + "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", "owner": "domenkozar", "repo": "nix", - "rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546", + "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", "type": "github" }, "original": { @@ -204,11 +204,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730531603, - "narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=", + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", "type": "github" }, "original": { @@ -220,14 +220,17 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1730504152, - "narHash": "sha256-lXvH/vOfb4aGYyvFmZK/HlsNsr/0CVWlwYvo2rxJk3s=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/cc2f28000298e1269cea6612cd06ec9979dd5d7f.tar.gz" + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" } }, "nixpkgs_2": { @@ -248,11 +251,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1716977621, - "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", + "lastModified": 1733477122, + "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", + "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", "type": "github" }, "original": { @@ -264,11 +267,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1732238832, - "narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=", + "lastModified": 1743568003, + "narHash": "sha256-ZID5T65E8ruHqWRcdvZLsczWDOAWIE7om+vQOREwiX0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d", + "rev": "b7ba7f9f45c5cd0d8625e9e217c28f8eb6a19a76", "type": "github" }, "original": { @@ -280,11 +283,11 @@ }, "nixpkgs_5": { "locked": { - "lastModified": 1731890469, - "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", + "lastModified": 1735554305, + "narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5083ec887760adfe12af64830a66807423a859a7", + "rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", "type": "github" }, "original": { @@ -323,11 +326,11 @@ "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1732643199, - "narHash": "sha256-uI7TXEb231o8dkwB5AUCecx3AQtosRmL6hKgnckvjps=", + "lastModified": 1743677901, + "narHash": "sha256-eWZln+k+L/VHO69tUTzEmgeDWNQNKIpSUa9nqQgBrSE=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "84637a7ab04179bdc42aa8fd0af1909fba76ad0c", + "rev": "57dabe2a6255bd6165b2437ff6c2d1f6ee78421a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 53fb5ca..a744ea5 100644 --- a/flake.nix +++ b/flake.nix @@ -97,22 +97,20 @@ name = "nix-ci"; fromImage = let hashes = { - "aarch64-linux" = "sha256-mfTNlGOpThanLlLQ2lL1RTcHqZJWdqUafYDZMeZPWEk="; - "x86_64-linux" = "sha256-DMlSaP+ZVqxd9NxdFydGyfkuJdmOW5jt5iM/7cDyTEM="; + "x86_64-linux" = "sha256-kJ7dqje5o1KPr3RDZ7/THbhMSoiCU1C/7HshDrNfwnM="; + "aarch64-linux" = "sha256-jz+Z3Ji+hy5d9ImOh/YOKCqy9P9/cseSov+5J/O95bg="; }; - # skopeo inspect --raw docker://nixpkgs/nix-flakes + # check digest of tags like nixos-24.11-aarch64-linux etc. digests = { - "aarch64-linux" = "sha256:5113a4d10dda16c30bf2e517f29a56890233d2660115003155aab1f7d279d8db"; - "x86_64-linux" = "sha256:be07ecf4b5c19be83f63b6f5c7f21bcaf19cf722d339c99cfe2b2ad09f81a7fa"; + "x86_64-linux" = "sha256:345f210dea4cbd049e2d01d13159c829066dfb6e273cdd49ea878186d17b19f7"; + "aarch64-linux" = "sha256:66163fdf446d851416dd4e9be28c0794d9c2550214a57a846957699a3f5747f6"; }; - sha256 = hashes.${system} or (throw "Unsupported system"); + hash = hashes.${system} or (throw "Unsupported system"); imageDigest = digests.${system} or (throw "Unsupported system"); in pkgs.dockerTools.pullImage { imageName = "nixpkgs/nix-flakes"; - inherit sha256 imageDigest; - finalImageName = "nixpkgs/nix-flakes"; - finalImageTag = "latest"; + inherit hash imageDigest; }; copyToRoot = pkgs.buildEnv { name = "image-root"; From 8dc2e7d772e87d3a5c9e05e599ae831e42721a91 Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 2 Apr 2025 22:07:38 +0200 Subject: [PATCH 046/102] chore: slim down job-deps as much as possible --- lib/flakeModule.nix | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index c2ba9f9..469fc3e 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -12,6 +12,15 @@ inherit (lib) isAttrs filterAttrs mapAttrs types mkOption toList; cfg = config.ci.config; + stdenvMinimal = pkgs.stdenvNoCC.override { + cc = null; + preHook = ""; + allowedRequisites = null; + initialPath = [pkgs.coreutils pkgs.findutils]; + shell = "/bin/sh"; + extraNativeBuildInputs = []; + }; + filterAttrsRec = pred: v: if isAttrs v then filterAttrs pred (mapAttrs (path: filterAttrsRec pred) v) @@ -209,11 +218,22 @@ ); in { name = "gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}"; - value = pkgs.writeShellScript "gitlab-ci-job-deps:${key}" '' - export PATH="${lib.makeBinPath job.nix.deps}:$PATH"; - # variables containing nix derivations: - ${variableExports} - ''; + value = stdenvMinimal.mkDerivation { + name = "gitlab-ci-job-deps-${key}"; + dontUnpack = true; + installPhase = let + script = '' + export PATH="${lib.makeBinPath job.nix.deps}:$PATH"; + # variables containing nix derivations: + ${variableExports} + ''; + in + # sh + '' + echo '${script}' > $out + chmod +x $out + ''; + }; }) jobs; # allows the user to directly run the script From 7cbd273de7669e243316f9a42f0d00c92da5593a Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 4 Apr 2025 20:29:32 +0200 Subject: [PATCH 047/102] fix(image): switch from busybox to coreutils busybox' diff took precedence over diffutils' which broke attic because of spaces in the output (/store paths did not match the valid regex anymore) --- .envrc | 5 +---- flake.nix | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.envrc b/.envrc index a2d9328..f990172 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1 @@ -if ! use flake . --impure -then - echo "devenv could not be build. The devenv environment was not loaded. Make the necessary changes to flake.nix and hit enter to try again." >&2 -fi +use flake . --impure --accept-flake-config diff --git a/flake.nix b/flake.nix index a744ea5..c262d6c 100644 --- a/flake.nix +++ b/flake.nix @@ -119,7 +119,7 @@ gitMinimal gnugrep gnused - busybox + coreutils diffutils cachix attic-client From 9cb54b33e82ebf2d5b582b244f39332f0e55b3f3 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 4 Apr 2025 21:08:36 +0200 Subject: [PATCH 048/102] fix: finalize_nix_ci echo added space to last derivation, fixed with -n --- scripts/finalize_nix_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh index 0c47039..f724d3f 100644 --- a/scripts/finalize_nix_ci.sh +++ b/scripts/finalize_nix_ci.sh @@ -12,7 +12,7 @@ echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0K if [ -z "$NIX_CI_DISABLE_CACHE" ]; then echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" - echo $NEW_PATHS | { + echo -n $NEW_PATHS | { case "$NIX_CI_CACHE_STRATEGY" in "runner") export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} From b193df30f4a61532f4a056cd759263f2ce453ead Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Fri, 4 Apr 2025 22:22:35 +0200 Subject: [PATCH 049/102] fix(script): quote NEW_PATHS to preserve newlines hopefully finally gets rid of spaces which break attic --- scripts/finalize_nix_ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh index f724d3f..baa6a0d 100644 --- a/scripts/finalize_nix_ci.sh +++ b/scripts/finalize_nix_ci.sh @@ -12,7 +12,7 @@ echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0K if [ -z "$NIX_CI_DISABLE_CACHE" ]; then echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" - echo -n $NEW_PATHS | { + echo -n "$NEW_PATHS" | { case "$NIX_CI_CACHE_STRATEGY" in "runner") export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} From fa6c454b1440cb7728f9eb630493194765e04aee Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 2 May 2025 17:29:50 +0200 Subject: [PATCH 050/102] chore: update nixpkgs --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 92c0748..7896ed8 100644 --- a/flake.lock +++ b/flake.lock @@ -267,11 +267,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1743568003, - "narHash": "sha256-ZID5T65E8ruHqWRcdvZLsczWDOAWIE7om+vQOREwiX0=", + "lastModified": 1745377448, + "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b7ba7f9f45c5cd0d8625e9e217c28f8eb6a19a76", + "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", "type": "github" }, "original": { From cf80010d07e32df7f707ca41461d158163b0733a Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 2 May 2025 17:30:06 +0200 Subject: [PATCH 051/102] docs: write docs & improve tooling Squashed commit of the following: commit 86eadd3ec42b7bce0dc5716d65798af95d0d8cbc Author: technofab Date: Fri May 2 17:10:33 2025 +0200 docs(README): fix built with nix badge commit f50057da69e89974f17bc37b5e140b2ef9f817f6 Author: technofab Date: Fri May 2 16:09:00 2025 +0200 ci: change back rule so docs only get deployed on main commit ce02b043f4bd83c36285e5620e71701fc3bcc998 Author: technofab Date: Fri May 2 16:08:10 2025 +0200 docs: write docs and improve formatter etc. commit e996b23cf877d8021759b782aa5996f5e2bf12ac Author: technofab Date: Fri May 2 16:07:56 2025 +0200 docs: update README commit 650f97b5608c32cf6cf66cc3fdd0965dc42e4860 Author: technofab Date: Wed Apr 23 21:05:14 2025 +0200 docs: add favicon commit 67e1bfecbcaf0b8f7dad2eecfaccf774cc560874 Author: technofab Date: Wed Apr 23 20:53:44 2025 +0200 docs: initial setup --- README.md | 9 ++- docs/caching.md | 50 +++++++++++++ docs/cicd_component.md | 41 +++++++++++ docs/environment_variables.md | 107 ++++++++++++++++++++++++++++ docs/examples.md | 18 +++++ docs/images/favicon.png | Bin 0 -> 1995 bytes docs/images/logo.png | Bin 0 -> 40993 bytes docs/index.md | 11 +++ docs/kubernetes_runner.md | 38 ++++++++++ docs/multi_pipeline.md | 31 ++++++++ docs/setup.md | 75 ++++++++++++++++++++ docs/usage.md | 33 +++++++++ docs/utilities.md | 59 ++++++++++++++++ flake.lock | 34 +++++++++ flake.nix | 128 +++++++++++++++++++++++++++++++++- 15 files changed, 631 insertions(+), 3 deletions(-) create mode 100644 docs/caching.md create mode 100644 docs/cicd_component.md create mode 100644 docs/environment_variables.md create mode 100644 docs/examples.md create mode 100644 docs/images/favicon.png create mode 100644 docs/images/logo.png create mode 100644 docs/index.md create mode 100644 docs/kubernetes_runner.md create mode 100644 docs/multi_pipeline.md create mode 100644 docs/setup.md create mode 100644 docs/usage.md create mode 100644 docs/utilities.md diff --git a/README.md b/README.md index 61eac0c..ceaca42 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# Nix Gitlab CI +# Nix GitLab CI + +[![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org) +[![pipeline status](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/commits/main) +![License: MIT](https://img.shields.io/gitlab/license/technofab/nix-gitlab-ci) +[![Latest Release](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/releases) +[![Support me](https://img.shields.io/badge/Support-me-orange)](https://tec.tf/#support) +[![Docs](https://img.shields.io/badge/Read-Docs-orange)](https://nix-gitlab-ci.projects.tf) Flake module which allows generating a `.gitlab-ci.yml` from Nix. diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 0000000..0a84110 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,50 @@ +# Caching + +Nix GitLab CI supports several caching mechanisms to speed up your pipelines. + +## GitLab Runner Cache + +The runner cache strategy copies the new store paths into a directory `.nix-cache`, +which is then saved in the regular GitLab cache (technically runner cache). +It's also configured as a substituter automatically. + +To enable, set the cache strategy to `runner`. + +Configure it using these environment variables: + +- `RUNNER_CACHE`: path to the runner cache (default `.nix-cache`) + +!!! warning + + This is very inefficient and should probably only be used for very very small + dependency counts. Otherwise it takes an eternity to save to cache. + +## Cachix + +Cachix is a hosted binary cache service that can significantly speed up Nix +builds by sharing build results. + +To enable, set the cache strategy to `attic`. + +Configure it using these environment variables: + +- `CACHIX_CACHE`: name of the cache to use +- (`CACHIX_AUTH_TOKEN`): cachix client itself uses this for authentication + +!!! warning + + Cachix has not been tested. Feedback is appreciated :) + +## Attic (Self-Hosted Cache) + +Attic is a self-hosted, deduplicating binary cache. It's a great option if you +want more control over your caching infrastructure and to have the cache closer +to your runners. + +To enable, set the cache strategy to `attic`. + +Configure it using these environment variables: + +- `ATTIC_SERVER`: URL of the server +- `ATTIC_CACHE`: name of the cache to use +- `ATTIC_TOKEN`: auth token from the attic server diff --git a/docs/cicd_component.md b/docs/cicd_component.md new file mode 100644 index 0000000..2fe2ae2 --- /dev/null +++ b/docs/cicd_component.md @@ -0,0 +1,41 @@ +# CI/CD Component + +The CI/CD Component has some inputs which configure defaults for Nix GitLab CI. + +## `version` + +- Type: `string` + +Which version of the Nix CI image to use. Using a tag/version is recommended. +Will not do anything if a custom image is specified using `NIX_CI_IMAGE`. + +## `cache_strategy` + +- Type: `string` +- Default: `"auto"` +- Options: `auto` | `none` | `runner` | `cachix` | `attic` + +Sets the default caching strategy. + +- `auto`: dynamically selects the best strategy for every job based on env variables +- `none`: disables caching +- `runner`, `cachix` & `attic`: forces every job to use this strategy + +Can be overridden by `NIX_CI_CACHE_STRATEGY`, see [Environment Variables](./environment_variables.md). + +## `cache_files` + +- Type: `array` (of strings) +- Default: `["flake.nix", "flake.lock"]` + +Files to use as the cache key for the generated pipeline yaml. +If you use a file like `ci.nix` to define CI, add that here for example. +This makes sure that changes to your Nix CI configuration will invalidate the cache, +otherwise an old pipeline yaml might be used. + +!!! warning + + The value of this is used in `cache:key:files`, which currently only supports + a max of 2 entries. So use something like `["flake.*", "ci.nix"]` to match + `flake.lock`, `flake.nix` and `ci.nix`. + See [gitlab-org/gitlab#301161](https://gitlab.com/gitlab-org/gitlab/-/issues/301161) diff --git a/docs/environment_variables.md b/docs/environment_variables.md new file mode 100644 index 0000000..56c0ba5 --- /dev/null +++ b/docs/environment_variables.md @@ -0,0 +1,107 @@ +# Environment Variables + +Nix GitLab CI is mostly controlled using environment variables. +This page outlines all the variables and their use case. + +## `NIX_CI_IMAGE` + +| | | +| ----------- | -------------------------------------------------------------------------- | +| Default | `registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci@$[[ inputs.version ]]` | +| Description | Image to use for the jobs | + +## `NIX_CI_PIPELINE_NAME` + +| | | +| ----------- | ------------------------------------------------- | +| Default | N/A | +| Description | Explicitly request a pipeline to be built and ran | +| See also | [Multi Pipeline](./multi_pipeline.md) | + +## `NIX_CI_DEFAULT_SOURCES` + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Default | `.*` | +| Description | Regex to match `$CI_PIPELINE_SOURCE` against. If it matches, the `default` pipeline will be ran, otherwise `$CI_PIPELINE_SOURCE` | +| See also | [Multi Pipeline](./multi_pipeline.md) | + +## `NIX_CI_FORCE_BUILD` + +| | | +| ----------- | -------------------------------------------------------------------------------------- | +| Default | N/A | +| Description | Set to any non-empty value to force the `nix-ci:build` job to freshly build the config | +| See also | [Caching](./caching.md) | + +## `NIX_CI_DISABLE_CACHE` + +| | | +| ----------- | ------------------------------------------------------ | +| Default | N/A | +| Description | Set to any non-empty value to disable caching for jobs | +| See also | [Caching](./caching.md) | + +## `NIX_CI_CACHE_STRATEGY` + +| | | +| ----------- | --------------------------------------------------------------------------------- | +| Default | `$[[ inputs.cache_strategy ]]` -> defaults to `auto` | +| Description | Caching strategy to use. `auto` will select the strategy based on runner settings | +| See also | [Caching](./caching.md) | + +## `NIX_CI_RUNNER_CACHE_STRATEGY` + +| | | +| ----------- | ---------------------------------------------------------------- | +| Default | N/A | +| Description | Every runner can set it's own preferred cache strategy with this | +| See also | [Caching](./caching.md) | + +## `NIX_CI_DEFAULT_CACHE_STRATEGY` + +| | | +| ----------- | ------------------------------------------------------------------------------------------------- | +| Default | `none` | +| Description | If no runner cache strategy is set and the main strategy is set to auto, this will be the default | +| See also | [Caching](./caching.md) | + +## `RUNNER_CACHE` + +| | | +| ----------- | -------------------------------------- | +| Default | `.nix-cache` | +| Description | Path to directory for the runner cache | +| See also | [Caching](./caching.md) | + +## `CACHIX_CACHE` + +| | | +| ----------- | ------------------------------- | +| Default | N/A | +| Description | Name of the cachix cache to use | +| See also | [Caching](./caching.md) | + +## `ATTIC_CACHE` + +| | | +| ----------- | ------------------------------ | +| Default | N/A | +| Description | Name of the attic cache to use | +| See also | [Caching](./caching.md) | + +## `ATTIC_SERVER` + +| | | +| ----------- | ----------------------- | +| Default | N/A | +| Description | URL of the attic server | +| See also | [Caching](./caching.md) | + +## `ATTIC_TOKEN` + +| | | +| ----------- | ------------------------------- | +| Default | N/A | +| Description | API token from the attic server | +| See also | [Caching](./caching.md) | diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..6f82a63 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,18 @@ +# Example Configs + +## V2 + +- [TECHNOFAB/nix-gitlab-ci](https://gitlab.com/TECHNOFAB/nix-gitlab-ci) + See `flake.nix` for some random example jobs. +- [TECHNOFAB/nixlets](https://gitlab.com/TECHNOFAB/nixlets) +- [TECHNOFAB/nixmkdocs](https://gitlab.com/TECHNOFAB/nixmkdocs) +- [TECHNOFAB/tofunix](https://gitlab.com/TECHNOFAB/tofunix) + +## Old / V1 + +- [TECHNOFAB/coder-templates](https://gitlab.com/TECHNOFAB/coder-templates) + +!!! note + + Feel free to edit this page and add your project if you're using + Nix GitLab CI :) diff --git a/docs/images/favicon.png b/docs/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..135b03026d6a5ffb7cbeec80c97224e5b6eb776d GIT binary patch literal 1995 zcmV;+2Q>JJP)Ilb@!-~pgw+p^fd`_57-dFY%EX0E7d_G7}<8 z{n2aqBQ$P)exR!E0QLd}$?1jt8xb1_N?<%jn!WaNwNK(1q7lBs^1kPvDpBkHxAB)z>XxxIyZ|R}wo{HJ=6Dn{QP$^KDk_0i-n|xe#zGce`^2N_Ptw zDEXr1=DW_~F|+p=Ci*yHh#-gvi1COZqY!b4$Pi#E$TNu4h`6^C5qiZ&jdwT$=As_6 zp(v|?4?(}kOzKkXFF;3A6Q-I!0NE&R1t=hx=O$kKqR-&6cb0>(BJJT-(IV_~8%7XI z?yFBu|FPH5s1u-Ft>t^CWj(!CwC+VNWX2%Vhj9s@l!Lmhp3vrv0RDQ zt){tQaM?SFz+^CIqPn@kyfv<0t9!QMeB~yu1ywhR)=dW{0naw{p}YjR#-|qs-9ZfM z&s5EBKFU=v{{T#}ghmgo{$`)5S&dE3`rC2xKW86DwGgGYbxqYCV7#?%immwfwBqtEUqHey!46|F!zx?@zlaR@^9f%hE^4G)@= z^2Sq(K|k`Q8}yIbU-Wr$df`rBw8gJb=#{f9_RMW#@GsdD{?r)%xA8Hr`dEF!&NoL+ zzg3WDynmt?be4}%rY5HsZU9Ev_!R<`-@444da{?piXl6VEsc+H+-lwPD-%nBWFPj# z#PZ`MK!uI>;i7Hb?n3J9-W$Lj%ylRy z7w#B8^U61srL|Wi$a9^#jROT2swl0g#lwEEMG%7`LOE1k!{IAeN$e0#haksIT7z2X z^^o=v-G1D9$!xfCAcRvjwY>FpB~P>syMfDquy#sVYm)#6PUTXPs(Aq9r-E3; zUO=QL%3L7R12D7^V4j~dKoo!x0O4&-LR!M5(*~C>)dR4+M;jiE2)$+Pa)u}o_bZM- zwaJ-Ls`|BfZz`{5M5w+IgQ{7;aFq6-#*G_*u0RW*tU=&9pr=0-k74zxJGb4D@d(kAlMmb!s<`2n2`e zZ8Iw(Qfb}MY-+k-#~UO50c77%yY2L7q(OyW_u?jKG7y>%eP1qlMo|A}T9U zicnpZIkuZ`d%h3Ym~eUS#NxUku|)A_qOH$$-<`VxUD$mtEOBsJ-|b_pL~GxO$|DME z{Z5is#zbFxdE2q+ihkC}^e+G>FneazkRff022I@9>CKOTB|i7=bs%F=t1TPLTLx|h zhBo;*1`F1v#?;=`(+5D@WLIC{pB6w5T?D4(j>tMI=zCVja~gPV*43N?qQ()RyA8}w zfKToHPIhX{XLtQJ6F1o}Xr2{w9{SKQ z>f_d=Jf!~UHSWNGu2qsc+4%8!>r?aW>u0!%#sWDueI-Dj?9`Z==CWX{4IDroVrJo0;=pnpU}rrFnLr}kE0 zo|V}EpCasS)(6bE$*xE%8!PkHrylZ|>k9M)>a9_pw?6gXzI)7Y6+H}usd94d3o&K) d-jDAQ@IM<&9OUDTXUhNp002ovPDHLkV1kV3&P@OS literal 0 HcmV?d00001 diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2a70bf66f5680e7b7c454e0a850d5ce731562603 GIT binary patch literal 40993 zcmV*GKxw~;P)1ui6Tyv#FFfuL{1Cjtu4!TGI18!?j@Z>NzR#` zGo3j7$4n;aC}+-@)0XY@WRiG`mpE>D({y66owni_RW!xd*6HSbD#TM!C5|5ty*=Hs=iuPuN9GN0W1}fHLBX_ zZ3;kY{$0H>{a!@CpV$6(^`7WY@ZUv5092phzq|b_MWfM>>NO%Fs+#`f^K1VseOCK* zbA2aj&QIw2)L(b?8P#j5bDHD6wmvt1S@G}c8vMO=_nXhpKO+h5lR0O1UI^|1qGu7j z1|6O?-#^sd%V6z*uZ^B_bWX+d;O9u+xnD%~tLoDLc8JJ#4)n{n{w*XZ7=%?#%jQIr{@Y1DAmo8km@HbTTH_2!VLyT@pv@3U5oYCim2JDTg z(@n11WJCnbX0uvI;ex6=N531-_0VfPfuP2yv~%R5?bk{BAI6wLo<;zn6|R9tdUBCSoyUTEmYB!p*>T;>v4KjovHNq3Hu(;liPmi1QKZIe=^A9E_CjR2Q z8t}@MD}MvP`vEMiyXfd)`&~TR(K{>zso9Y`?#@A&gn91fY`h)4OW;jvJ$0F&_5o^#sEVP?|Dq98U{J;+^@;LK}t|AW^gzs7tT6c<$lrMQ6X%^-Y zw%0ia_*9~Uk#`Ei^~DK1*cbra^s)ot+`w7+$HPBpkf6dunNA!uMD#k!#Tx?e{qwcSuIK4> zKLOw>x)5G4ysm0G2sU1<2G7M_{lQ+TYe0f1v5p{J=`Mg#5{41}_*J&TFO(w~p z=uy@Ii;-dZ+Y{br7y63dl!GrbIUb88Cz$^S*kFIz4p1O?*b6m?J0B`8;@5l7? z^jBZB0S^xk|G9|#W?f#uWdrucHTepcV4|cx+-;pPbV3Nl+pl{{e&-jrHl@RjE_Q7n z0d&Yq?5~H)BTlv)3eb6DXvSuYrj|6$T?!*P1lEy-i1}qh`ysP z%qkXd89YcG*wZ0TFDx$r+a$mvWR6hSL`hM^)Cr%caT5MM_~_8TkMF_j`uqFWOixdL z?gbn0@bK`THyVw1`*Wwcj@;PTSY=c`xGT7N; zgT1}jXBw>qEuxrmErBC4UwaRtD1KcLO7jz_sR2t}*gYTor>-nm_dO!=>bZ=#YmTt_ zH+c7P7Z{?6!9~^OxP|fCs88lHR1NZ0=@d@+QbVVlH@K#{d`#(t%c!FU&~WD?$wQVO zVm*C+okR<-W|2qa7iov`*@S#sDz@20xWZeE_@9gHs`ujtOf*=8pBeT zhen~ZJGF9sdhkuk+`n)%abcjI$V2bHu2VSORR>s!$p68>K#&{O0Zh@&tOGMTku&_m zstaG_d6r=6>cE~s73N1Cn^2StF6XZm(3bN+CdCIcJF2E){)SM3zRSp}A_db+z8QB6m&QwXU|@$q2PvZ(&VN zYRUj*?KUX+R3wRHIJWKzGG1xttYw}U5I%y9uZ%WYhFU(rlEk19p`&K!D{HdNCf9&< zjyh=yw_Q!t(kl5{s@k#^33&?FYiO%Q_*oZKMeCvybv6J(BWG00AoIkXnoXu7`KQDZ z&3Qd7BG>s6QzHOEw*y#O6KeV!<98&j__@^?@eS}E&!!qE7+g`^j4#mrUTSW*#CR{= zwOs#;j2A+6*a{L###A|yDzD2YpyD^l_PufWRr+2jL-o7?WG`ElgOqMMUsvm1&f(HT zgk#6*8ms)*c=ceX7k7@r4nC@*ie0Hl8(#a|6@WIIpx+z7F8U~62iEfbYP8O35M=eF z59dg5y&=utk z;@MX0CF7_AMNY#Ax!(XhidodAvsX=?K`i;gFEZgeUr^}Rxd0FG;W*`q&g(_1L&zY@ zEuEtTm+aN-lwoKp5#W+9c$UEkf*K}e_&Bf4(I$t_GCYphu(KCgx{kuN1sTBfBB5*l`1(o5s;o-EPUaim8)U z0h^W##gZM;T96Wz0NOc3v3pjbH+ycaB4E4C3Wx1!aYdYDIyDVEu~JMa?j!^EV>SR& zLaMWnm>S3+T|1t4>@NM_aG(|JR&R6HF>ZMsd1rLOYPj)e~9`#L-SGGNDT~y zyOrv?3P8r<&rKPjC(q{Bn2=+LfDr;LG(9IZbTx77PV3eR=8t$s(VTX`fY2e8GuIOV zmE;to_Z0?Sm3NZ?uUfV0CID9i-l_np<|+f#>`uNOW_CT77jw~DJCj~Ya3SdXEK60) zhO(ci#nzb1BYeKv_9AM34K0aF>Q7X8!>B@IA<&QBT%omaT}uo?so3y4v*Y{%wOsM) zbIoE+=bJsZ@x(qH3!i&wfZkG4+QI>j;hiO9FO}ZJY)EsECg8f+YJg4#kBmd6{Ulzv`C2gB+8E~`N{3ZV(HO1)&NJ0&PLfu9nH4GOGzSC1O zBk}_xB8M*@ITlb_ir%}cN<`k6tG&8HT`fj*VT^s#X)8vs2UX+pgcGzAqypSV^0v;}s!0Np&VhH?Q<8CtxqHeHwROHs11A$J*5 zHJb|Wb=sa57S#?)q%8&%Zx~X>({SEHi-nfC(HeSIBD9D{q;q40zyfrod!f(J>@^EG z-ltsbc&l7lXanY4Sll_g5L{#)^hR_x{KzA#D@96&$Wzckj;8}N(54ju=U!Fc1OU?3 zu-fhokW?wcFYln9pmVXKJlZ!$|1ahw62wH3I^=n7fu*?N!tjngl3aLQgiZE&vv>hg zKu!v$xX&KwDu^gC4Yrnu6I_2?1AcW~NPOyg)H(xYF}hyJ0x@9(nbl@Cyp&QpGDn+SO{j zYKUG(MfAEht(x63{i1*ftDnW`gT#@lxhVDC)1(IWcS;4tVz>33SwRU$DEBoY0p1A2 z73T#$tt{*5>HD(RGtKSQ^YCRiVxSqaJRv_Qa&lTPT2srWpKE*LjI7YCDGtFqJey*z z_;5C8n?;R0l6D|E84iQ*dg=h~%t;z)pZm27RQ1N1&x@#H>A5&4-h0+ zRBl((2<7!CH_;0XKi9?nGO}?i29{mv+wr1sD0SUEJ^lbD9^DRK(*=jB3>pQcf)0Xh z;?P-T(5L-9GP+?4Mz7ui0i?;145amfDsAAa62ZWV5rR{REy9qo2%y0DzHi~-uiaAz zu!!=eL|sfim!!?A)+<-%zx?ubxN^(uq7X~mDW~7>zkdgwJn*n^$i$mw%JXB?*?87E zQN^f2!Rwj!t0*Fs&Xu>Kn&TrOL46D4s~3pKQpBx#QdUt?s39Gc{lXB|wNbGVqlSTk zXyF7NDlZrcpOh$iOeCnb!G!}uc*Sde8FL-}+Lm6n1;-!zCQi?utUfD|Gz{cO-Y(kc z48bF#%$ktrAUw|;Vqoko??oVgRkZWgG@s9Ttl4rC_V3z(*^^TdI?A3%s@k_(uL5ym zMb6TBt$y`wz&rpkf9~DA0~p=74KAe2d2LX`O-EGCLDpSo{*sd@EWFo!A5RE_Xw!<9A4oDJk z-c#zlN)4RuSu5%`2)MAixW@_EBo8bMB$Q%W62Ry>{HoVs=v6nRMu2&UlbLI}{JOVe z;lL0-bst)lD2B@)~C3TAl z!UmfN3D!U?0L^w)!Iz~v5#Bmld+cF6@a2!y?KDNUXLall8m*RO4P5_=zsr$MiVz&i!p6d9A+U_J zkQY1lmiJ+BX!M0yuPS;L4Pfle?-jRwWswBxda4om;dQJ52}3GlWq%Oh8fr>f^>WL_ z(smtGa8%ENZ&CfM;oNv9ugcBhQlq{Y?G}+&jW!46a|w1fEy8oYNnX~5#pvj6t-AVO zV&T9r6biR((Sp`GG_t*zfyJvfV#&xRPY29sYul5Gk*1q=NMYKMl#t2I|ASwmH`e3P7b=!GrjtFkn%=8J__367qmSaaF=m69ew_1Rk z-us`>(?19*A4!oKaL(_UIk5f5v=i&k{nnqPsJ4drYVSL30RFGACZsATJ`*2*{I7!Lb<^~D2 zYDwQ+iqe^IaQD3*!_=`uwP^>uHTuAY7teznXX;uEJ_v6Ta>FQrL9wFBk+g_d$VwEpS`e?LhEs15~TjzTTeu!F`t^C4-Z`JN;|N)B9%U^ zVP8q`KK{39LQof~qEzuu`RLFqZ^YtN8)BN750iz;$C+m3HhPQPj%?V9g#*Jv+!8hdp>BXjPIpnk|}2G;;s$PYFP& zx_BQowJU>QlKb-spQ2;~DF7VW_a&U3nXb6P8Ff~!PpK()Ac$s!!Z+Uao6tgEM{hYITBGJAs9((H14O?*Kme&cl zC~8n+4G266RV=%F9oF5jtq7%ZN<;a5-9=Q_A!U8OdhJ7ByT_9r3Zs&;%-Fh_Rljyw z-;Zz{4XP()uetL>HG|WNXw|2qemyk;^sG)$t`;8mjST_2B$6Hx#Cllvskc~27RX$Ry&8&4)_h8{s zf9eXx9ASvj%xYr^VPsGu)`~{^9CzRQah-bd5VI^iQ%qbZk}ZYC$aEAL780`a7|~N= z1r$a{l2o1AdK*PUfSX3pL0vwolNGy^Z~FHLwT3ga_{Zn5pk7v*A2>rXcCq5))A=r4u10;4y1W4OczSJ3_h1ptWuUk*aDVnpz$W zMGn3!GMfhdLM#oMgQw>W%G z+D@(aU3TqlSkTj7LY6kCDr(q?;Nehr7MFu1OJq0aB!;S7xRV?3n^jmR78C(b^^za^ z`FEkGe~_;!N!zj-N43!ot{By^cG;+9aAbHS{1*m4it<@g;V0AlfL)*c5Kc^t*G7{< z<5pn-$9OSUjzYB_RB$15kSF^(usU$K_~ik zqgqAIVX2|m1-J$PlXAn*Vnm&4D(Rd!vKJ=~?=7Nt$!z^rSX@|})~ZZ%nMMFC=;_1i z&2Q(AC{;4J=g!sPi%rq?1anQ(54KK=<%vDt()~NPhXjZ+MDv-6*4Dkb;i`Ml0IvUq z-?c^+%xkJ1;n-W>Cj%=+s4OJry{c+9Oo_y-)qz?R%&7vC6Hnm5ul+av2!pm-Sx(Zb zc9jUDK5DCrb*igndu1)u=C)2ZSt7uaBp8gA>*i!|8o=Xq;f4fbMWv?jT|3dxP38+{ z|8Tw2=L+1cVDy-sXJVS=qyOW-AX4iQvBX|&THM-=`yg+x$?&UJk8IeAzU6D8;5&M< z73)gHBAOiu&YTBEo7p%1@{e@pwn?59W6C-N?Jsyf)axe za>xAIhrfOgo;>g{mvz!q2nXsQVkH%`h+5}cG@{hp46P$E!Ojb-tICA=bs!Vvu_J9n z;aeoc-T^U=w3v%NNnfrqEnfj5z@tfAiy9Z=!&p7qarwR<-uES(o}G3@M4>8_8lWY0 zDDmvFQtQ|pi$^wM@v4mpE=!7(S)SEXNR8S^McWX5fV>I#he1;bt36=%7eAs?$HwPr zm~nPfdS)whGrHl&v1ZHbQrJn7z1rqGp@|$O#F`0S8`!xFEyL!MK7GL#Ump)rewik0(VfguX|&KnSfm zWO%~DA#nD_TqN}wP(hX`!Ubx;t|{Q_>6s~g;=7-A!IOsjRC^?V-CvAXc7zc6h*#b4 z>*CZb429B#!s@zrq>VP)O^eG%tv?MCSKUVeFmrMeyFPt)$NKMFP?t96e#cM#!tbEB ze=#Bp7;CF~RAF=Nd1!7@lt4FBJ$Oi5^#|_#7$zsiQOX4d4JpA?0ax$!9d;IbU@(fE zSR7qPO%2h0QWx@jzf{zM8;B)5!~s%T13-zdAsgkwazvsAK%=@VNb)C+{gulS6k=A0uFGTb@TJ)FhIhhKY{%aEJ}e%-vdsGnKuzs6Fxt7w7?DB=|LmHde-{Qg>QJB9yvR7MWB^01Y z>@k!X2nFLQ0<@~K5~mw(2Cx}s#sHSsZz_N)R>KS@L9|`j_`aQZ_^Y2wp}vnc=0cvM z@`g|Ddiw@3_OJeRiU78}^4v4m|wzdvfl}lP{dO zyFrpmuCo_N)S#4_xW@=W-^zOj0`)~}Y!ouHXn;qc1FxKxKqG>tQR)IkHDL*WEb%$A z8_F3z9m-<`tWHT(JPxLn8xL$5EY*aBNFAth$+B!9KlsMoj+bHE*La4P=NL1tqaLkp zt*CZc=@#@|az)q#UqE73xuSsYT=RpW-nYf9001BWNklT3Pj+jT&y)f%9WKGxr`4I>-2U|{)3i|=e8hGt4j#3TeXLd6w6w8@-x7M^dN!+jsQ zE45a#fT07JxCz9=CE1Ab0xFvt{IEUC(31c>WzSn1u?AuqiVjjcJ`V<}FH?=L3P z2i(ozWTLq({%3W=pEH_TvA&dL{h4DRC4^7f%f&TXazyG$^%LK#*yZ%>v;e@8k&Ttv zl@k#>#^+cDQjj{TkSo7u(bCIg;*srMn7rJft1!w8Fjb~zit~!;=YEJa->2s3Sy|B2 zi_r}~USC?$Ip$0I&^F44hc2y}|81TGab=)jajMIC+j+6=B5U1=hrfOg9{Tz{a3f1n z%pEOYDaV{zRnWOLdAW6q`!I{|>9F=|TV+u*KrmosC7;&_2o`n1XAZM?iJBT5zr1!WTKTcWO0!|Iy7bGIg6i7z z6`b>CC$;OdAH>wLLrL6#Ha6Z`h5D&ehQxG7muBVEs{_c#z3^%~#LT*OY{sG#6wD|N zr^fQkF)xyzh#V2qBk;IJC4P2F@taaYAk!1VTScq3FyTB2l2fxe#A6DXiZMbean$B9&ccI zV2rwPE?z9(JxHYuFwdMZ6-EGf`0M`-haTJ|Vxte?b>@e=k#QNM8PS+%KUBc+#Hu>6 znQ>J)2nH$Y^Ik;LA7F>0sVgO^uL<*J-v}sNI)`!AQyi@qmqKjIQmevF4vFelbdd(U zDSVw73I+*=h4b`xCB(OS!K44<12|m)T0xr`U$U0Oy&4qP9wQsKV#&xR(`RZn8Mi>0 zVdg&BF}DrPFL_+Y%n$)^|EE8I=h|hZ0-*EKtk+Xt9N#9T zvDo!DAM(@1QWzY_-;_hOeXDOdeStSd9%Nmxp$zNKI;03NQN**WJ?kvSc@+F| zb`aI9BU*174G#~RbBIioW--HpP#%AXoMG6c5J~pd$kl>~C0aD@d$kM#oLD#4k5v@u z5Jp2BYEK5h_g0FJqFk1YErn2yV-~4{MQW9j_k1COgdE!YMVy+Mj+(lQf6q)E#WRQZ z;KboQn11p=Ts3oSrq*nw4Ol#~DFvMZ+qCcMSLHMUm?`$cv8I{PRI;>^=lI?iKcdI> zKL~ZQG!}hrx(jh3FB_onB!wCb!64E3n3%ibeBVwSJMa)@PEMu`Z))NY?*HtE zawWrV#GORkOvOhX-oT)rI&Z8@4k;Nt;wl6}$eu>2eY7Mx5H#QlFwu~+)?ZexT-mGv zs|-oNab?4>S*`AhXLK76>3F4Xg>U^8}Z7Y z`8Q~$289fuPafWj$G`I_oIJiCr)N*Hv1!l1kStlX5tm)}HuMe*qr^Kh+i7NM0=xhA zcXDf^2e9%&3Vl@_Wv5Pg+^fZ?Dn>W_IJW-BKMOgpb%jHrT6ysJ(x)_}VH?sZ?hz5p zOiyC>mp_V!zIKmJ9UCvcY2@lHSpV8>*nj^H?BBJcNFN+2Cff8&xlrubmm-C7f`NX> zfg+kQ>PtcNIl<@7!IUY|OE1e)(lb}BTBXr^Tu)ErhO^0qpu8G6-? za_P0VVd3Bq;o^1y9sKqu^@(qPGPXnV$}*33qG(hs$`Lk2?zQdr{vV8P*kZhILmG)5 zno)aDjaOq@Y2Bk&`@3C#b2lEi_hWc&dQu%$VV+q*8V)$x>>Pxob`4dx!^VahIVmH2 zU$Dgpg=HYhmKtN-qPY(ZszAjIGF64FT)9$fJa6rW?^XDWZV%wmV4)-*&I4O7%9_u% zsTr4Cwi#XR@=H+U=1i^_BBZ6SAkUmKHL+Wvpzo5)F|y%bLIE86{#Wt+fA~Q7`H=xt zfA8sEp&Nhx52SZ+IP{sPXQy%Cn}2RfPRfdv9E&RLTZti3?fi+RP(xNMIj6_>e-o!? zr!hD*inspZC(+Y4Sk(0!=^ke`RP8rt$E+4@G70Ye(>pP~_glq81C3zhgh5E%M&&pD z7FtX=hsQ?A5=^AX?|14m5`^7Qi#OC7qu*3K_0xOVXp%(T0D4)ua;46(6GStNv_^5y zhNJlmq;4QuCnjH(YaPO?Z~bF>j>2(^^H=%rd12(+-VkYp#kCGiOq~-DJaup<9@+jU zVc6Ch$kjlz*;Lp0lDtXpzz{b4{QG6$;L46u0=d3oL(Y;V$wWs!e^?BrG6EwX?PlCg*)x~n-AjnV-JhEaX3|V!N|ROXpCmX zFpDk{KydfpfNKb5U@JQVmLqPffAZ2~H(^v&JRzfODDWjiA5AzimxQy5{rYU6Y=DPD zzdFU}W{8-B>ZK-xk&+FIO6ZNlmE?VwU->g>f`YYGYm8}$i!}yU32uAXrkOIB|7)7h zeB!&G#_ZHYF2nF38&wQ;2{sS*U~_-B7w6ycO^w^_2P?lxY09#s|r)`^6?gCP! zT2i1~q$pY(32(!Ko_@UYrhgBpV)o=D?*H@$it{VNWpz>nrs5k;837=ee)14@edYs9 zJ`o=dkMuNT^FTL7db+|*RdTfeQR53r7L`;bBgyNICnJ~=ljbrD7WDQPj5zV38Nh9; zR$_`p9<7!Z=0g_ndT@0!i5C5;Vz(1LJ=TxQIjYh|-QfEc^5w`?vDh$xnhUV|m0*Wj7)-T2;@ zKB8-W{B<&N^%k~@{M`UlG%RN&Ei2*g02&RT*;MTO%m;i)CDjx+4|L6(#Rkjha$lQB834_X#1!sA5!&aQ0J&Ex>_jscUk?1g`B=wJA zwy4%nDkI{Q;Y*v@NWlv<0JQ4J9tLTnx~Unw1v`1c*BzAE*}N+*U-Q!#9KOo3pSZ6C zHO=O>=7*BJ>hzYxw|KUGcl@!Ph>Ge8;O@1T7~^eukS^S@svmcZ40v`J0AT9)em!w$ zj|mXDL7hFa@r0z&rE1d%&o9-Y^TNK~=g7^b;#D{PMq21QKm7sQ6oAr;mtTr9g>eY= z_}*P1rS`@py}0{Tml)#(fU$ud+`aY^VQ)mVR8YsB)*K5v6QF>|IJJ!;|LFjimp zcC5axHDx1^pRF(5ML3ab7tIUqKvFDl`w}GU1fg4*RmI8@7;tc<&c|t|kJ=)@6%|1;@u7tNO8dphtp- zn>wCym~4TksM9AA^J()ElSZTj!y*N4qJ#uModD<=7{cf^w+a9p+PhPJ_^q#$Id0{} zOSyjwB9c*8o51+qotQc{&J7jGQ~cJY1GV>Yq^AqNclltkO;9twnKBpzU1pl2cug_p z9@7V0cI|BdfCWAMSbg2wQ8Oo21JJ=#@mX1hNge^gCL}F&kgzg2uyMH9}FZpD(3Ya|4na?}|O`DQT7U`3iW z-ii(wH#=B^HebWjX!uEKRGZd0;`31RgKwVG!xDH7D@}K2kKfzB0xQ@3tjV?N#wxj$ z8ivds=~dsXUWBEJW8eR(xlz?o2eSBM>+(gR;^WNJggiHSv^~0|gu9q6XN?nPez6H; zO#0x$-KgzP934{?UEO`SY|A?Uv}O!F@THFgI_he+!;E}uITnYFnZ&|6#z zQg__GVv(6m#Qm^QnWWVy+ekOXcFUCASDbdO`N>}?qP^AE-Db%edF_b0QW6ngM15#S z8gH=1T}qU=Fq!7V(nh8ZFIBU&omVATL_YA-4CM+!IufUR#O&DJL zIt2Eh24yZmBpWeT_1luytaa$KS;rJ5SLe`(_?M1$R>#sJQ+LzU%Z<_WN{OY38AXuZ z{^h!S%}?7+g}HV#QKb;JQ!-R>WhQ7yGb?gxZQ{h?eY`Pn`>))%WPS!*g@5XxZdKK1 z4(+j`Cc#+m>TM!YIeyAOVR5x0Nlp3>4@-w?AX`=G$9S9i~s{H~!-P)cS!s zb}tZ@0akyj!e>uR*fQ8VXZXyU;pTzvj*}hKr#b|f`AUiAKdbN?3m31%>b3}fRCB%Z zCw>JBdiupxIXqqz3D@PQw&;RQ5dtIYUdl=wfMrtLhI9SJB*Ac2utR^G@SNcmYbm3u z9J+XE&CyM76kos9+=kHp+NR69f;w&?D=cLm$73Yv-XGofWy8v;PH5}$MHuXwKc#i+ z@L6mgGdd7Zw8%p%56o&CH2%VT|B}&buzbx=KtyE! z&g~fA`z?1)4u4dy^K6MD<9m1Vnn(}(mgV!`2m#=xCB1&8G>>X6V$lL{&7fx1gr;#c zmtA{X=zXil*VWyJk&RnTN>a!z*Px_Sw#o&5=0D^?1?oz#b74^0h!SI)o|YvvuA`ah zWz_m4K|34?)_LSm1dyedUWeste#+1W6`{ z;kGtsdg0)(FmM%tJtBhD*WQNSfguz~wiN?VH_@-`^`($on5pHO__fSy%Tzxo6^Rps zE8(KDfst_@pcKxgDe4*u1}V^j?BT|2aK2XLw@pVq3~H!c^Q75D3WYXHfM@)>pEdoW z)t{riT^M_wi>~d3g*T+?fleOYV}KQ{nE_evKsV+aK3B(Jm!JT>TuoDX+GX1_H(HGe zrc)^6nJxFQ`ntE*L^HWoUah%Tz4q7h;Bwpgs$L^n9ifRq0M_S9MjdOe*M?n{B1be5 zG-9p*R(&th0J7vNH_gzE=-`P9y5h>8Y5`Xi?y2Se)zor{Q6-*R67zwfbp`@V9@%Ga zTzfCOeMKK$$gz2#ha1n+eQt%(M-3w@^u}tDa_-m2AfdQ%C|a<&ids1PPX`V`6(iT& zn);dV|C_rpYvgwjb)GmH1MtJ0+l!&)A|hLtFM2`u^2Q~-wcpOqt8LZNGQOq(RK1?w z@VeK_u=BpYM}e2ol95eVvT9Reaj{B_XBc{PgFEper>M;)&rS)df#s0)MjhJYP!_^{r>Ha2Y9ciI4r@Yj|#ILMbm4 zZENq|u(bCDExNJ69*p*MG5*b|!&=J%d{Fk5%DW|EZb4N1w>B>1qFGaF zL3bb4T>q;8fZ3C+#(NkrEQ;Fm25{`LhtO`)S5ga+Rw=IMh26`69;{uoz<4%x1Ox_2 zA*Sf&{Z>_w)z`kgHt&sgD^(l^_JhRphGF;;2#CIcFqhOAu|^)am?ZA< z;BpFp9WfE@ihf*#7ocMq8!W%#di3-yPkE{`>j!fXMG55Mx~lEs(xl%UeLxj|Jo){v zg9QmH(!q88&p+fd`5Sp0Chs3V@U1jth_73z?pr-_r74OwM=l>h=dPz0qp zyW%xiJhBl0upm$&xqZ_w|Wi!~c$Vas_blQ&MEFy{cPw}3(fwJG1L!Ojo zB#I*s7OO%C7ptdnj3k<^F0QY(uActo7}>b308=z`@ydU5?OK?(Yt)OWtWwm{?0pen zc6tKO9Nwo8X=@SZ$Fuc$BBo&&U8W>Dd3cYUIdu}bAuz~))frz<{xP(2!1Xgnqe4A` zkxM~Jn*{(|w)ysyYi@5(b*MWRq5NoM>cFLLt+9;l)$S5I=OIA!x11_2(d@g0jl>sb?|=hJfTD? z>FvVSWs6?aq8l6R!C;pZvx*haV~>2*dP5&dYe@?YU7tn`A8JO!7^8N%vN}>nb(5Ej zY{IH*wu!U>t!k=6q`h4I{mI83!sNs_4EMNgKsPLXaRxltCD=09Bjr>cwV*H-nXJU1w}FoD>B zu2o97aBx_L*S zno_T>NUiBls$Kc`okHwtQoNg~k?;2_e(HU#1<=%jd8LKYM*AK#JF7iu;So%h4{m6v z^qwYY!XRc|ZB7!Sl^X?J=;vz9O2DsEz9Qb6|S=5u@+YL-k?qYJK zWW8u=5dXZW?THgBr8--CdoSd9F1g|-wWn{HLzsi{m8vj0RT50Iv(@rIzqy@bHU!** zTh)b$_so%ntXs6;tauCn*gV*S!LEkSiCT#5_rF@oKAXPhEGj3Ua*t6?=V+t}%Y?R( zM`V0vT7_-7Zu8T(p?7e&r6~ECyIb8#K|V5dY#hh-KNv3f8<(CP0|tP13@s9+c}K3N z-AroGYzVS)-M>Kp@+)gWs<{9^UpMbx{tAq2+*+y0n9ebxHiq#&jna{t%}|`YZ83OZ zL69_iK&y}{F*~0@j8Y^fK_Zbxx>TFep}k*deLzFXI^tY6Sm7#qb~0U&6p1j(+tQ2@Dhx%|fk6TtO!Y7VQ1MSjOb*>YS?2t3F(-boYZF>H?IB*u0(j~&I z6W<1F6|?dQkxA9i7+95qR>VHDK`Zsptk<&R+94coBniuLAZR^=l;KLvF2V6U9bd%14W0&E`W#{HAC zA_4gVRv8r#!RpQLNK{B~xCzy5NDV+7fV%D9clQq9vg>Zc55M{6hKKFtXX@sG8A3xn zD(#00B2g)$9fopH2B?IR8n`Ci41u&n#B!eI#xkno1{X-OXPhuP;YV`%i{$g*f!+l3 zE5dOXAhkr@x@^%|3!e=dy`bT6@$&t0}S}@&8CKi z8k1u3dq+2J!;+Crc1W3JD&}<90Uq4BP**lWAP~uAq_PPya>+EQ${BdVjHS^qGn2G% zs#-2W=RDM&kN6SiF4xImTSgsC(#?{unl~14cZADga?A3CI2T97=QU$4gFTCXbEQ028vs@=P+pQSGUjjJ{2j8T#7RE;ckus;qx6+uoyt@nnT!Sp+U(hjgM5- zRx|!U&p9#R3dj*Q+@Jx~b=f5EYQ~1LLSm${?{%d3neem`$F3tFMA$F_5eD7y!QaE| z^dtnpft}k;`(E{|UMLN9?wW8TZ;000nYA*o;uW%T-M^Ug^EB3NPJdQZ<+_^j_@2)f z3ai`{1K-r(?o&db8x2#M4Q2-$6k2KVs?Ukk*M!TJ;$w=s0Z<%{s|kS(fx)44B|U>r z#m=*JgppX!P0Z-mXs3P0Sq86c7~PA3MmwmGj;NGH=4FV-q;9G3xKW5&)xmJacQps%!|}XM6UYT|4@r4 zN8Yz?pq`VSGQT_cf)E&l001BWNklzTb0Jl-Ax?zT)5&1+@Q$r0s%yTsw zaj|KYgCAGj&tQPQa-q% z&#;3`?f~;Z48JcD@@km`;4liyV5spP$Z|5psN7--yr(F7@oHxzyTANV9N4uT(S060 zN8vNqghzY2goP40f*V@hlI)mM*lRGWX2p+F-t>KxE&9U1b@NugUgZmOHsPWr--kDfN35U_ zb z(+a68b&w5F3dZ3yJTTNH-VtsTs*RpBL|ujREw;ZEp4ubQK~v}7+t7J zxRMR1YT!4WRXvTfUQ3U`ENOLom1R(Q{QghWqRR!96zJH@6a8$@mwwOYljSU8i^#pb(b%CMMod7ji4)zRs~C>et*J6Y{ByP(wNUS@(p zl@p7r8aPbhyxz|{M*6)1s6qtK9C`q!+g>;Vt0GWpzD*5BAKBgluFUvC2Os6Aikp2r zJ1a$r;yrY^nTQ9mimJ5#4s7J(r)vU7*zM zu(n2=>T0Pat6hqF$8i67TX5_8y0N%b!;HdtAMe#6DH6=&Fr6&s?)z2hCM}|7sGZpA zg772_j*?u-@Vtc2>3%L+fWd}DGk$UCfqPK)%+LRb0I->{&o1nJ zG?gD8rHUJNUQ7^WmYy%YvX>MocOOy9Y_jV2;S3gd1I;0t`||2$uy_FPPzh-yqB=@M z1|C^Wn-PsN1)xn;tncf_xkO8&8s?3cER>pxn;SAL<@h}Iw<3}EwfU-@MxkCkF$peI zO)Q4384PkSS9Qiq0F3l>>DXY;dA*-oS1gifi1*P)zA|UyLMOl~iX@Ld@)bNcc|?g2 zBpmYkCJ}ct)scCRb zF*RU`8K6RA?Qv7D%%P?fqgC3^?DnDa=PfvlcBL1s`P0n5HMcQ9?x{S{Nzp+#d{d~&sBBvV2_;d`&o(JnS4_fGbfJVnL`hBoU@hNXte;eyz$RWP2jQn z|GESbQ4p63Nt{4JL<68wojrt;8mb)FAlnEe9V4a>^`ilj-F(L-u-35c8*uq6tk2dlo;e9w(^CnM*!<9{IWU3}hR~!zt`n(4iIcPl#floC+%9j8N=crJ-Elg1 z-BdTnbbzYrEz1_3^V)A+*hd=&%k300EQY&nfe=O6>>biwjf$cj>Yg1t*LgVytHNVf ze^x&4N+zi_7YY~Rs2Uvn%9wcQ@6pm9vxcfQ(Muh_(_9h;|b;-Bnm-jm%B+Cyl&89b<#b4Koyroz+N1ga!FyNNDlMW zmqn@&T|%)yTG;J;_y&q5-p5F z@N5g(M7gP9av-l0q&_a^#yC}s;-4lNJchk0aX`GqGu5`l55JWv=Di$ zJdv=h!z&LeZXY8(jdRwxFfRjMx)6h1qP{u7jaf(`a?|z;o00^*PihPrLH!%>ZH+9& zn!T$Zz?G0_q3BTn*>(ZKXRZlfH_*)+U^?xm9{Z;`b6^+C=BN;gLwoMQbCXAu1s#Q2 zY+)6NYy(d&gW9S9>vv_YIgBy8wa(|d{R%LzP?poY&SsjH@JV&`B$K)Z|~ti|~ME;JgtxI)%||`?`%)H67Zbw(u{0@U!@QH3nAr-lTlCw!z%j zl|9H=)>m5Sb8i{@YzoeHw-PB-71Chp*8#zUb7cR)UXiX`;{lC zDQ}g%#+mq0)Cr_Sf-l&ybwyvIE>bxtDQ`6ynjCfM_6kQx9mGw;=OlJSCiKJ$~$ zsTUsZ<5E~!Lx&!?$EgYm>hREtf%V+vQ5<>XE5<4Ld*)iYRm)p#`gt9#RVls8(QJ8A zg|>wSu>#6r5N=drY~pu!?k6TnXzmhfG)MqO!un*9L}%}AqJcR92NIVreR(Scy3pY> zhtcc$x=rjuh`h;zx%5B)pzrHVdRv+!rHCuYXegpy6Se)v`42nP3sBWIIpw0br?(IH zm8C}jr)Q`2nL`iQiSa0P1R3{v@bCW+;#UyL?k$q?$vR{fe6n_7Syts}BI#e#lb2vxRUOW2deKm1(H40&@0YWi$NM=(M>R#B-c4sQNOjE;x+Gj=0(QMsBpUk6&sLX-l+ifo9M=`Q@r#VV#wK>Nj zrVqH$;9XSQKyj7aD4^a0Z64^s#dNIg>&Du?ZoGV)z2ogeeb_ZM+oH`@PD}*P9NMj? zXQyOAPk);;Az=fU2=LhbA5ZSMQd~77K71!1uRRnj#wrH|0aHw9l5kdcweAV0s+^^1 zW{CQzG}GkbVJ*4+PdmZ$tg++}Q7tSv{*$@X`Da!}u-uIbk&xDLJ{uQaJ}w-X)vgJP z3v=br1E1GA)TMgy!EgUHW~L@k$s`LQ!oHlTB_SgsL8U5;_-2yUI<`|LsMO@`hN+ZH zYb3vOAq(9eVtRn_AMz6psEw6@~;%+yglw(AqVPr%O(6QlByBSEQEka5G#a~(>G zrUoUPqWE5-QVQ2~CDsqgrx5UpIYjW~9M-ksRw>CG=#-W@;jg;RB8fukS}7Qpn*+c* zR`$KTx_?BD8(TA}3Y7HZ@%{45p$FoLmI@ra|E~=|*JxxYExGgcMsX-CQV>LY&9ddQCF!1I$rD88xIU^ni*?x@{`Ku?`wSM>9Wij4uP`hm5}WNh%_ z_rQ~<6_aPqoTXf_3xf;foPGZb`TX;GFV8gbroB&M@^sTIMG=uTKk>^L*|e31F_+T_ z;OvuB;pS4fbSYsdT_qwo`sj8%^7a24f?}FU<)2$+(-xqXe!swfkQuF!n;Mdd4GxWB z>@Dxb;EItsjTI74eQS5I!0B@oxodS!i-#^v#;#y)E>Tj`9ar49^N+(hLLqL}*c5i1n8E#%GiI^9*kh!30X}f`Fh+alA9?)gf#dl6v6J(9?H!jc#vPX}xro86 z2G%ZG(DK6VwN|Z*;|Kl;BOAAxxmmfr%F-~nHK!UmP_L#_<78^Iw}xJM6OKN*1J4|K zAT4YsV|> zShj#W{FU0q7=Nw|VQ^?v*59xViaBIAJ1Crna}!ZIVIOuIm)Z_r#6%9QUmiQ~AWlq- z8{S%V{K7T880v4L*#u5KD|l{7@Z1UD%&7|({{5#P9LMJ;rq9cF-F4t;jL)9IotH2D zXKk?3P%K;w^!6!w`hf-Aij`P^#Sb*(#3{`^yGwO6H+f;wGk`Vz8*&&`HGlwt$U%FjZ`wD{$0O4=ZST^u>p!bpD8pNWOaXwx6)_;z5R+ME1OukwuzN%&tUP&=6UzdAAbDAc{ASR?GH`j zjo&$h$ndi(32+s&(c!k*fb#}A0j6m8UJ=7VWG zQLAz%l8l!ZvekUEi`@QfLC*k2H*N)?&!vz8`Pl|jxPlIosuM!S-u#|gklObih#zr6 zB6KBCm5rQ4Wx>QvN%(T1Tb?z`+piyga98miB7*Bz&W~r@y-=}qwB?zXjyBJkAzvut z{paFs2Zt1^t~!GiYdRaPi6Do&vAA2niW&fLW_B7UkMFNt-wJh{e&x=vOn`1HcW|o6 zwCPf&R|=(H09bYPzr@0UVSrRVi_Ei)L^s&VO-DXi=cb$AiNT@Kj)cZ9 z`cPFq$DfwHKYY7JbBNzpw$urIK9yTpbY)dPu ziOGraFrR-R#(ORedDn@Vm#y(U8*X)MUOnQ|KNS&adEjyqwGWMH(Lp=lHvE32aM=dC zS~$V#)k{`w#Nt&Oye1&3Y(OtLU(aZ#_+7M zW)4bqT9O)6)d}8mV$iB9KY8GxBFPsKd9en3whj4_>Dia9a-K*3va@5j$$o8D1YR-cyz1bwkqp>J;Tx8oUFu}tQbljSVsfH^lTSbY z64d0G=U3+)?fuc~oa*UQELqh=_gNPB*kCvFlrf%qZj!`R6+9D;gscZuEglZtr*|)`3z#)>4&l)j)hx)zjEv*2}9?OUf2!H+o|DgC(k*5bZM9rxrX~_;gC%tujTY%hv;=8Xo9)Vq#LlcDUr46;YGK%MX{psa zA$an@KUJY<9w$qFt6}}Tg_3vQ`*A#Z;9(;-0;7P-^LagdPMpyIph{Al+5(|6%B&@5 zUWL`x7Z-casb{Csw~(5MErfF)oQA| z@#0m@)*XHMctN@D&?}ngIuEKsOL~GapdxZ+b{a&1AcIl|Y*mf!4&!lnP)&mn1(%h& zhniBAsL!+~YM!_63^kxD zIJBvTtP!g5n$fyS+UNOy#;y`y-;3<Qioj1~!8WIn>m! z<3sPqbJHinOD@nh&jW?Hok`-GX)50cXl)8RPujFow(=A)nezoXy1Id(S2WST>}4aK zZ^*oG@B-W`v9lR@S*tZGWSQo2?dEw^j4@U8kPxb+uES*R3sHzl9S@v#Sgq%mag}CdP=Gsi|D0c>KGcviY&q zk2X6=!%2S-B5ny8yTAM~oH{u7>iCN_dBDq5k(5{}o?4cR&VPLgr(&NGEdA^?qz!Vk? zGo$=yLpb=IPnjrzW^YU3T#*NSCzG`EG_1n%!~gJkjPKi30|>NEzESi-!tH88Y{(#~ zPq{43t`;*>f_6oVM)wbm*y3~h+@6`eu%T?DE2Gpu$|3XeH5Vpiw%+#)O~1$_riBAT zQaBwdl?t5l22GbKdIADUW-xA{a9k7L-+||*j+zO$$~#6d7rJ33XH%Vt_9Ytqpo(3e z{a|3**$S)XrV`A8L2N82sFWP8hzJ(;4I1IQiY~YJ>C+d*B2FRokB}&) zG!#omTdbgo8v(Yk2WN+IDD3Zxb(+pByt$A+hC?5OH zrxe8)sxTF-oUJTyFnN>%8xGd^zFm0eYxl@iH*7=1oT^?-Dx8*0WrXbDat1$Df!?g# z))nw6Rk8f?^*p|@8kQ`3p1OE?U%R@2-X9TJ>h6WDe7EZ&ni#X^>C*-js#a2?r%z8D z-b+*L+4HnkX;*JQy1M&Jh|9&(0*>v3!SE0&0FQt7(=9fbjcju^9yAjx+M;$w36FGI zlU7yERRyZL%DyyYF+)Q>XFrasET*!QqR;>+7^^JYI26AKZzjAK&Ha#Nb7! zmJMZBAuDT$I6+!5pA~iW6{XKC#hi!zSB6mBpB;WyrZl5IwL-3m_&sxS z5;G?!lfb)L_BP)LaRbsyXI;tZws(S`{f$47;=&z2d&TAJ(9<`7+3886Ja}SOvFEAN zI9pvwFCU#l=5xtqe(@goj9EL(EfyZ1TM&8K9|LCUCQAKhCudw3uX+dM^x4_wQlQ;z z^5{|#k)(-LYRtJgr-YXY3A2+enV8*7G-Mv9xd4|^D3P_94YuyJx5|o^cZI3L875fk zeJd_sUnD82s`ozratq(NPY=#Lm-)pU_Z>Zj$x|w3P@bKC%tOi$ps~f^ZdZOy2)7u; ztUNV}uyV0cqU9X> z0q711E)26hu+L(z`7@Q)zkQ<_DGF3ZUwywro2ws%Txe&|R=t422tN7bd3lOF8GL$Q zqa61Lzxz=$!q}VNgMlO>Z#zA-si2-~0Kn+gV@ZaD4g9_%r)mrMFCQ=N=v#X6pv;|) ze>{51jNyrLP-04ivxrqrgO$`7>uik=z6{A{U{Qy4ogXdnh zwsVB6U+l-d2WN3`>P)Sl63#(gWhQf$#0on3*D*naP;O=HsIvz*C0n7WfIpK>u)&^H z>Ey%NFfL^Epm910F>rd~Vo2s#Cy-$Y&KQ$B7CNz zG)e-tz}4*Dqw;!e})D-%neJDvcuLNM!V?F|7NUtwxV02L^Ui z@!zxVwOctyPA{K41xXL`C2}kr#LHaj7kNBBbq4o7K3g~->K^Jyn%7KbN}gD9u9{Lu zu8DG(>&EZ;usk&q2~$0C9G}%mu)`gDXZcdevCkQ5zJx{Mo)|@lx`_-3wcaA3c398}SQ0?%Mrq z)hVQP4KB$O)3AG9pQ)LFyk!kTibGv9tJiyDp?a=MB@_0f6rWQ{nEaptu_SlO=V3z` z(gu8OtRh(F7QU3QgFLIrylt+`1@%<2T7Sc>GPq)-?D#++$H_5}q(B{GZ+@>SFI9K% z@#!Z1?7^2-F5J}(tXR{;;LyuP{CtmtQ)lqWAI?~`*<&{)fFl$k=5kJ>!Cuh5C09<( z$9p0O17LIxDkWG3PDRBwG#C%8z9wc8PFt^%APlJdpK=cDU(4(+=Ag1%rU0^dXcU{@ zct-&=m?;pDLaov$hNxoXs?8X^dW-Sw)$wN!J!=4sm&!3X)Wq^DnlFdc&*OO8-TX|wu_0E5sdPy$G z-u_ldeKC$nz1WUF-F*^|PoH51VVT5<(RxcTO;mNMz$-O_^~y;g!qH&Hjs&X0bBHVF zNcCc7MVehAIP-);M}fTBKEM>jvm;Gp zndAT`qnSCL36PUK*Ts;t>E{&@T(5uUor0jvA$1|OJ2*1IGp7TFRw{sK>$NxC@qT>l zx3?JrN&+{&cM>;W+JkeU8{O%6J~FqTeDqm-^3k)u(fsYTBejC@OI9fsJqh5*OVfBy z%r3AGQz}u>Q5%`m{*iYwNK#PNgE zP5j!|PMp`$(#GW%p8@yuwYbdl9o2cU9(R53B<_8D#&`Ot317>uA?87tusP|g5wD&_ z>0EP8YE|T>2F70)efHm3$moEzD}zDb{&2_4lg!*-qu&fI`tx*1$kgIK;; z`liiq{5332d}p|3XCFCmZf}(m%>L7~)wQ2|=bb<>RuTaAJaqRr1}LLKsJ+<_UZAZgTSL9L|J&rp6d1-M`+FhcFg7pAE} zVrk_lx)m3S`*Un~SUP}w4NHn)^K@bimT^vaT@&E;@M1X z7S`6H-B3f8Hn98Nk9*Y6yK7U5MxTk^JUUlJRIjWF(aOkB^FTLw~C-JAdpH0&^ zQpZv5m{TwMLY}eM)RhBC*9Xj!%c;IhPRrGP%|did4LO3fP7}eK6*e4KRZWu_j4n=C z=cNR(71hX+0k~@vm(hxS(FRw@;tfbo+}Lu)6cUN!+#jr0vnQU2b7j zgb`(uA}%bC=PIOp)nh&!mD5m!%FZo!1RskVSv<G5i>TA__|zG^?e9+DlRub& zZ_neE)1IOn4eOG|8W7A~?#YrEPfTYbA&a9bgP6N13Ucylx$k-0%~?3BIn{ zr;;X*gS%5K&y5T$QIhIpCDx}3VB7!lL2zeQCx65p(yC10;7+CXD@nC-*JnSdlgADr z#3F&Hy-OaSI-|e()f0Nx?vv-%3vU=W??(LopICzb`Y)Efzx%f zA~(&z|YzBEr#-0cpfcE!c*p zwhQDkkk~Q23|4WGT6t&Uo4fvJwqkJksv-hW8^N%<4SOv4~4TYBw@iE%ve-#?LV zbTt4ialV7}LGySzx$C>r__;4UbxzAncV0V)PyPJzb6$&Hy}S#b`o$IawGI6*dENW$Rwn52^D{-i?sn}E@ zzeWjk>A-aX1H>$m;|2-aIJPC(sx={f8nCJ^#Hfye6(bmH^PS5mmSY{sL{S8nk|!@_ zL2degU7!09W=>A#>!EzdJPcCZ^nk~nr%vN-|NUwGgDnGc^XlGnvLLTt)r9*S>ZFd!9OD z#UlZgN&q!bd_<|PVJMW!GgGp}6I)Dj-igy(_RTYAB8$>-)Jljrv$?zidZqLMmy#2~ zBirf9l`FY0Y=`!`casliI+hg!sN=iP?y}{LcVO&Iu>mLDXd-fCC42R1prtf*`>)f- z4&h_Jdn@O>&_Jn|LL4<9L2lwm5Bkn)7U9kxdkJ~4E&yu*EOtMi0k99iOGxMM-p6P0 zTi=+(iBoFxL2@DzXtt6Od1onN!Kb>VWbBmJ2+PbeJs7ds*mYL!Fy?#<+lq!e09qNa zFRj?KwscB=og%>wz!%8|Kd@p1@BHJ>1(s`Xl~PE|f_w!nt^H?Ho9)-P{{8XaxfPR7 z9*Q>-uv|P$9~c1zd_8G*-*8En{?{9q$oZyUKHsC+3pB0*P!}|uX?xPhN#nXTr(QSJQxO`Gd%hIAVWhxJ*8?_xPt05O+V!^|Hu)_-09Y_V z0@Sf8)Tg^uErw?M;UAnjBfqlaX}#-v(-&RH+r0RN=8PCavJ5k^9)UrfLA!Fr&QVj6;+w{thq~%t2!CViMZK$+JZlS z0dE!-Sr-7(H83#n{^AWjBKC2sDhlALjL(Y`6%m2wDYT(Oz7?} zm5^L`Z%*z0&;1$p-@hZF0lW*pzPH!VlR9{5@STLTu9ZxElL^R~lz18gGw|5x|zL2_Qzoxjtgk!D6S(#%NmjO2$N z$Ra}cfklE}JYqQo#-?^FV5p=B3-*TEEWr;_wF}v;LR9VI%GRz4fovr~#$~xyT_J|=_;2sJw4t1-S3`z z&iS3+`5h`oFpvt7r^iRJ=fQ8_sz3OgDhDwGr4kv=a}gR7N+N9LENRFue$Rv3RmU zy54w{`o2R`_~OqhE*D+KjTdeHATIsTZCJ3VJ!-V79BPH+Qa!=Nlo40X$qUh$8GF8W zm$Z{e0ZZ>iZ%ax&3ZX+s7R`oeLzIdfs+8h=6*CV;$$zN$U4TFt+9c6A0QB>J{x$}W z?S~BO2?ZsRLpqEYwu}JqKl#f&c<`;pw^7`Y0htRa31#91Z z8=70YWIZmY&1Ii)_wy5Y|LaWrP$Bc%&$01lk3|430kHgy37NP4tb#kAu9b*Ht|y(u z!qyI4a?|bDeDi-ob4y1I%~eL`bQrltDffhDMuPbkcPz1D>T8NsJN;FY^g;#92o|c|}8D_?ZF;)4M ztW>LyI6Ya#506jd!o^Kkzw~v2cY6V>d!2!&Qq2imGh!=~y zSJG9V{2w^~lK1-FHA)T$y{l*3(l|X28}s7h!+qHE{kt$dQL*nqF0op+%fspat<49M zFLnN0)uNl)+uOhBczXbDNNy1)Hz2X3^32dxu^XVu;3-;m;RbZBTqBZtjVP0`DRt9o zhPr>}!`Q#`5g8T8(H24Df*l2!=d;Cvs^K_Xoh52&Mn~@^EIt1+5D~@(53xk7N$rU1GSKuE)eyD*ZZ-g=81=KPT5=c9h$Lu_^_fLbabuL|DD~ zMqK%cyRf)>l`>wn7a9zc8_tuRzY+RZ`S14axR?76JR{L!C>g4vhd@qhAfx^Ykw?0O z6EBO;-lt?YHZ+;q(ZbWK$EK77sO}Dyle%@?b}rW6_3f`<35cC0Xb4xqke&c8Hj*ol+%`F{) z_k1-*nhL4sW5~>m@!>wa{Pg_V)mt~uAlo>GnLd4aa`*Q(HzT5J5wj^A^{J z_(oxYK!obqS)Lj>MV%MEbIutqYR8h^O_&}Z#&i)nv zy!Md#Smi7}@#F}$ADb#hbrxw@zU*yXE9shBzk*ep-V+UN8+&AxnZ|)E75{PkbAR#Z z*HJ!o!0kVj+=HMX>LAaF2-wRBl(Up|V0LUyGOoM{;*g<(d%Bs`Ghh|~(7>?+SpTl; zv8ZkCq@6>eStQSLbYW4~s|=jN6My@kql=Nv9(F~=1eev6%CekTH*7FtW_%cJ%iluH zt(|j=x3D!2smF&7tNF1a65x?(riYHs^%q~&GY>0l0&pRK^WVUbxsi~%3qW~(A@zNS zCUNUiqjS-S6g4znW%WC6q$@vh7g{@eEp5;>rnEuQYw*J4=Ga7p!6VP%xktVhj9;m} zj%|v`9^X(jUQq1hF?MMrqVwTql6n>h!CR%tfW0J`FbW{zsN}lM+eRYM{^wmJ9c*7atBnxMde_!P@uSh6SyiaMZ)| z5bN%rkK@XJ9BeG4UIkq5609b@Dalsh{2dWyvxoCFY(DtJm8=qC1(!91G=38zyIYu4;D`Gsj#H ztiniZWYG$j(0MisrwJ0en5AwE0~j4Rh4X)FGdh;9f)Mx9WdZTw`*%Kq-4AVp)Z~(A zI!P&&%-{-InXg-PfHnZsiZ}0L0f1%ckUB$|bKba>x2f%*wys`!@4x?B^t|=T zD0l?h?rKrxA`*-?(nY5-q?2FeiI48xj$?bbyWy;ikL7@FwFhl7D^GbnywjTlNEQRI z25eophlr8-okJQIwJ;`!XKKngIUaWEmwRZ#)gMGt^CBHKJCDcy`akg0cqOR)knp-D zv?)%1L#_=vLCi~xtSBajkD+Vzl|ZRUFJ^NqmaMuA05CCh*eAmSbY`0Qv18K|gw(Z= znJ#)0h0L!qq|QR-<6~9d7;5)w{k0#X-~Pidp`~Ma!@!egkXm?A0>kmLkZ;@}@SDAE zda{C_efLY4o~*#AXOQ(E?8oFVX=*^iH+*75Y_QOURuZvBA6mL{13YW4sPOADpOHhG zWF(w72`B)UXk_3NKmYi5R9m3;c>bTijmlu38Xo0LyvpblF-iei5+o+e|gva7#r@>ds$Q}Nbffm=(5MF z_OFwIGeP+hYnLTC@eoB{vU_og#wAY>ee}t?mJM$UbUulnWK7Iy|@${S_{H8t@0msfei<_T3gKaO3v*L^!V`%GIiQm2bUaY_N zV}Vh5p{;jFrjX=R9?XfnHyoR!NgMli+?%IXFoY%}1VGXTpba;e)L@5x3szV#HB=fi zGjmgWd;3-=B&jP92A}E@EjZiQgp6@zkFzt=7#-}x`rr9I(k^t)Ja>KPFL7%BUUl69 zmXikw!$@(Gw?Xtk=c1*=76ka|{%eBO9S^01}oU{4)RmmI{IYqnHq(OE&SIl*eO=|{8?MLtE&nkOsq>p|beHb_!8mUIS{cU%f3pOuwtl~Y zJO5z>BjZ)CW=G7-%`NS?^h3Af(wlBab4y1dy-s9yy zyt4N(GAWs6J83Sn28H#v)l82}lg&3NDduiXDPK~zhiS- zH&$JKGn$%Pli{AX*_xVL(cb$uRA;9!HF7H3scg$x#|O{ivFE3;>4F8gu=~v|+jDI2 zEI#`65p4hYRM7h>mv(XYDthl{{ti7Gt_*s!>va@0TJufuoKvrjah>J!Ki+m5PmT`7 zL@!v9{6!xCLsKlpEEXZ8*Vgf{2 zFQJNG#z%&*u&tfWTl;n}fCIZ8#S1&W1KWR1CZ|;zR(i#C=p#a9Mc~kflPr1BiVa(^ zc=?8T(e&2nPt68ez%bc#{JJs0pK@nNd4IJ)A;z`jNtfCRVkR*jko^Y zAH#3|!B(_HLS`8Tdq!xVi1_jP7`eOQ3I}|Gh=twZ|#j4rrp*YXkxNM?3x*Z>+A;`xg*R$EOK6)g0hX4 zmpLg+cMrl*M4^PO?!saKsLsyN?94PyAAW(4KKK8EdCP);LGqvwvMUOJvM93(gXDbI zqKhd%z*b#$Gc9Ug5#;SejdN|~^O-lE9v|ZA%4t|>m@E}J^s6fU@cC(6b#XJ!Tk`8I z4j&sli$DHm1rO|=WUt#LJ9yi=dii~y`8(=)>y?R?zxh3+pxmozYKVo^srPtk$GtfD z;!hMw1YYC5|0YC+*kRUyBHvr~6_De7b=leILI+Z?2vto`*ZoTYFA?He;dmuTCN0}& zG&i@lV`icP3H(@*Yjvajqbr})>aok55g2&`cIO3~Y1L&PX*gGFDXMy#AJe4jG9CU-w5Wm-fPG z`4}HQg&*H@dsg?=TFwURv9(tc0*aO>#EFpJfLG-FK9Kdwm{``k#X6A4@xVTO=NK{dWZ!cRVDgU|inQM@u#)gme8u$~KS%fr^%b9z}naMHCOjh)>&KEQSbuf`*H1A|%sIG$x=ODR`a)J4d6gp2P zr?{;8gIPwc{-VYj22^ORa*0~V!a5?h-aAw8vjBnZ_mxQW99mc#FzzmL%|kC=e;rD- z(77IUk0QnRM*hx2soR#V!^!9F!_4@wwp?)|zOsD+Cx#h!zQ1*DaM?L}-2H=b+`WC= z5&^POU;YJFz3ts}$&I%~uE!qfN$ZWS)9TGP;OOp0mB(^;3Cv+$X(f-zG(YG`ETVJG z>~|P%Kv~Z(2iVh`4N0bv9b9Jr#E&~}eFD)4NcQ-7B5>av@?)6vyQvcIdx*G;{K znX#~~8y&ryFgADyXQwK~@MzL9{V$xDVvlW;y-%>l!3ZX30At;Eb^1;fW)ASoPShik(KzrpYU+!CQiRR|ts z)0Q{2NJJsqA?Gg@7)Gi+FabitN?RdPAtEVir?|y1N&t~%x8)y*35B7kWR^1+K+G^$ zNX%c->h-z}D@T=Wo-0%l^ax55mdNiV<-CcA*1h|)XliZ^$O5mX95LUAMYo6oR6ccpS8x6x zMI#F424wcVJh=FG_eGm<_?ZWBc4mr$UUjq0y&i7QfP>0M<~~a$U8xWbzicG!#yG-gEsRBD61m3l=R|g^A%;xH>yY#xvyU#8WTL;Mh== zuD*By7Bx5ieYX8*5})~B6^u;iLDm*sWnGIT5MiR+N6Xh;j>TOoeNf0D9mTYlZFH0U z+_YdJ&dyF_@bI%Xw`340dGJdYm>W<#GVf9s?rj{kE6e!4|Kbrx3_nAq&|G%BVv7!3xt&xc; zZvOfRwmvYXWo`s>E~~u@338Q(Gc!N`@L#dFKN>0R#2OE5S!v^zThKOlND!GYm>eT; zz)wlVQq446pvdZL$|;*F_ZCr|vJYnhrk}m(35sNtL^xFDke?%I&03X`Y#UTunG(+L zNEE!I!_gvT14`EI!c)nW+OjV|5waIK4zxO^-9X)IuEvtywcA#oZF z<+MI4E0{eF2V8TRil!}AzOJdgz5R<;Y8AeiJM7G)Ahs@kum4U=YOoZ`*nvRK2?ldw zQK4OBrEYWdn!YANeyt^K4T`Mtg=vj=F`(VAHJsX6wjW^&!EvVe{ zax@Qlq!n)(VdnVsMF$i})8$8w<6;ju_8NHdD7mMnr;4}|d_dqpd}2cgXocc&dXq@f zgkWPuMR1{}9lEu(2y$8smhwD9?YWhgegvHtT=A;I+*h3o7nI51!0@IKWI^&55dZ)m zxJg7oRL{+f598SG|2@~9C=U@N$~O@rgc|TBNG&lKRFG+MBzEJLTd?u^Tk3#W4J`;_ zlCb3y`?2G%Z?$C?HhlOPofc;PZ`k0H*n5#LY3Y>O3Hf#2RBe}$)`V97ZtCdh*lI$L zAlbIv7ar&rX@Ans38ie(2|vm4ipfRGN>JuxvLz;ioJz`Dz%vEFT^DYqBD<*(dHYlu(cz+ zo_QT?X1@x-hL(7SM!h4>z9PG)++4nUSM%S&wGyqR?)c zGi3a_c3HA|LomWwB7s-hv@r6@?8xcZx|`F?t4ccZb37?ob-|f1p6C!UCnq_Y(E*qq zXW;>9AX|y$>$fDqsCtv|8U<=309Ge$3idub8pQyxV#D>g;GLhq!nUpgurr6QJbX<| zrdmZM2Y@2IZjJ_8-o3TwzW>UNtsNr){;2YjRjk;! zs!{TCa$2B>2F+H{_e+NHwHr%5H^T01Vqr{?gWjw5D4pn1A|zBYvd&FaB6zWiv^AC> zM9~e&mUX3ieI;V(5W<;b1{Er1vb=%aYpy}on>(M;l2$w|Wt1}$!x-&t!o?}dj^otdnQUdMmtsE4Q8wXA#3ZCKQ?QoUO4X>WEF1)Qj0caoW> zXGNOiSOngE|Cg0#ZKRpL9>`^Xzy6w=vABCxAPMa91xz%SPec%d?@VdVi~fQrmF0=( zC_gHoHHhRDkgG03T#KE~z&7&VLUh?;T=Dy#&m*!NP!kb^ zj27JY)fh@p!(li=@?t!X1t*N!GaG4GrjXB&*L59CDe&UX^b?Z0&^AXP?>@`Z0gj$D zLald=7KX0GE|5*mpA~MsSjF}nx9>0fW zN6&m8b*~e^^0k*^#oEiYkRi0Zus7gCNWs&>J@oSMPbh8%%|k}w zN`23g^`MEK;MfF0Hm|Yqo~oM#T_6bheP!LuMM2$)cf|1Xjf-ITH&(yv-v-jK_0<1U z9@xv*%E$L$cCwsN05Ll~f&LdC=0-MfJumN-!m{7>Uix8_7PQ2!OM6|BC$ez`95Ri< zjvyj4&zdkZS)msnzlYPQqwiUR>yE575n=O(|6BuUMGh)E)R^p{9@d0O424I41dGX& z2}E5Y|}+ZB9=B8Lp!~iujWy6*5j6= z(xi~`0+}ps4iP#pxB|_sok?^#rBB?5=F-f>Fa{1hAt6-|5zd_0L*qk-lg1gFju8#< z&F>brb)$RTwOX`3i!}6v2+rxGkyIoXKq!O>iA_2o0o82nU*2^;jSruaEb97UvbXN$ zkd=Se+S!A*Tyrx>hocNXRa2mx2jbX!07oy5>KCucnO`Pz+GNEd!V7nG>q?G?<$9$M z_)QINDJfBw37Z@XM4I7?2SDY@T0UR(VL>3yGHfCMv|tgh5Pau)`B$v;BY&<|tHEoA z4)4Ou#IRh@{P(BlG_V>n&PI@H2Xnx(HP=Wl%#PAYZq5mXN8)e-0^8D_X?S2Nm52cB z{@z`6Yru_gqXX$#;k7ni_bD{DvDXnSTFf*q^ZFc%T1RG8C3=8qRE6#mb9S@g_cBN{uTurivjSm z)mImit_|>-WBF$aC7m80$^k3IjLK&13>~I3$M-Z0^3=F(LX}=#A){cc(joKV?D>*&@(|pyrCJ$)oDKqH4V&Pb1w0lCP`*7}$a5^OLu^t&3J%yrrQ!ZxBw^$x-F|^zVD5_^xtO zir4?@z!O{(v1=HSjnzg&R?*zQmtL?LZOhi_<^j#n()Y?ks&#b69taZj4-0lwURH`= z2^8-p3$B~ZfbQ&X*I#opmabeMfXEgv$$-+#2ovcMu{HJ(2yGCMI$gNJw4MPNhYmb5^|Bn9iHOFA`|#2e_eL8!)LV(Sl+U@?gL$t@ZunCfJ&>(-a0o1UXNK+ip>XCb z$v>c2NNmD!S>3cGEZa0xuZ+gh1G7_;VpArvOdgIH6d54QOYrra;cQ{UC_tY6){0`& z63Nrn&3@=OZzGnhdPfqBY9yph4K-tCoIbFFrJ@Mwh7QG%96GWSGZW<`*P5A8tyaa) z+AGB(i0rwxW$W@L4X={Gmdv3;t)dJE!(q2Z!o?&j0e&$69N76?%ubDgtJU@5n*@@wz zb||JIl9O9hXQy!bz!Uis;$Q3M5TmV#DMn+ia03SCZe zywl8Nh4(#iZ$nKDjr3Z3Avh7?oge-JS%L!TGGo4JTzxz{#zp4MhCSt>%od1psK;=e zVXUoCQCvkdm%nYvDgAcB=WXN;fz(Vgi6O$9H35_(-&_8#`1e@xAPGP0cOnUVE)- zYTyXt%5bHN$U&%fP!T4}dqpBJWO0o|MD+5m`%pf4py5NO5hJ(vqP4RJ8?XB$?9g{c z*ve-(51f)ec4U>ks*6e+BcU*xsEM4kCy+!J{piX@OZC2xuy{+WwEsq`sln8a;Z%=o zJVH*%0>Sa52_mj#E^O<XHwuKu3;Qb(P4P&I`e;Jl8`jGU2@;}Yr22RrvVrKQWp+w1rYlB8M%nWRcixA1w^EF) z2v-6}-lwtQQ#iQmeu;m|$$|y1+5n9dzPTH1Mi_`r=WDTVZKsnL!Pd$B-AN6BB8SlC zQZ73MQWr!C8kNV9?{VhD9?VRZ zZCS+%q!S#a3vUd|?93$k_dUWMP^N?_(S<@54!J)u~ybqgQr%@Y8GB9!1X#a~eF?=NQxpe@Z1jEb%THR}| zAytnw+QSB)U=M1QtP65xfDJrVc8wD;CX4gDE-5Ih*}PUQISp@3KZ^7pb8d z7@U4&m8a^3q~Na0yio9ftFu!WI=XA#<4`R7r+j=5$|v`#I;R+a8hBOK0FhKv4B{~& zqOqaFJd}{%X@me_uhIMc`Imk~5jHq7jM(WhC7X6B@K6ZF%fl>Z;nwcS>BG-r;PA6i z4c4%?-@oox_HM`6@TuZHiL}ltv6ziVPBRWUAuU_^D^(_RD95l(70xnxETvs=ZnK5*97pBLD=T)At z9^mxB9kwP1C735~hL$O1*;sEBA7#0QP9NOib}P-d43@gaw(hl9ylfrW30efdGsyrG zoOR^{4dUS;ES-2RvY+(~PuG#(NfA_Lr^fK&j(a(fr;?p)&RJwHE9gcl_AYyPjiUh< zfL}I3G-0}JXduKy0@Xe&uP*OqPSzuAs0j*3j!_5}Q9*Oaf4ynRL6ABods$5uyYS zv!1Ii{cs@h$ue)$y4~;&M=L1jh3{<3i?!#*%Y8Vw>pmoa(j-Xt^3(TYqTH`kLLz@m ziT$WZ7FMjUUZhy9R@n|hGai@%RhpPwlr9mn7mn9H<4B2!hIPH3>&W3qxbGo)r1-}YnINLE;UmaoZDJeW+YHm=!x%iWOQE1f^h{DD^O_15 z!UN=;fN=D)8NkrdU6`3D8{nOhIvE>Lbj-?3%!~ysomf8CBh4a5EkfuFZ_xeO+3Q$T z8SGqLjX`K0u-aTN@46o|laTxlza#%`X1U~{g-n&ye44NU+nxC`&s;Q^l^SWY zBAHKeEKk>#_6Ze6kxU2Coe zA+lB`KAM>rR$)65q~k1l$k!uor$88jH_FcHI(subE_4M@I z0pKgCFPHkF5f%Ah#*+O)RwdCp2<*k#Q{LfmlV}2u882j=nC;JIqG06k(a(tsQzUsg z{!-_{b|_b|Ny3vV_B~kU`l)phJ3^IJQqu>50oqwDI~>jx#JFx;=#9Q4LQQ&y>Qwg(jyD`R!3U&X0kK8)&pe21j%aJ$ksa1+%6eB z?9@$63mkixfK#T*nNQv{0Mq72q-P}uWe}JYi9Lkhl7yMh$znuL-oS!ZWlVs<|qvY*V>EGFW0Gt z6CKFVR3iUD6ohaXcCa)=3}y=Mc^E~==aZAU&F9KdoD=~x$?>{U<4V4d#t%Yo$MqNp2!igi~@v-5}mB9SvqB4Cl1=|&5M@Ro(F(Jk>gvD6Mzz_O)SJtfdgv6 z>_TN%Q$w5C8+PqDt)O$tGxms(yi{S35Ts=4Y^OCwxc>_cC*~Jt{JLLH?``AM#>#-g-aE6P;DZjQmOPU^~XIuJ;OxQ>4d}7 zNH}W;NY@Jw5kSPP=7U{an9tjVQVqGjfAqs+&T$e zV;N1D@Rv+95!9#ILB|P>`ivd=ZR?k^224RyAS-oB`+YPH;Uevcq1e`ox#H=Gjx@K& z*HljwZXy27qOX{Z=MR=vc0ol#>aD@P?dVFPB_)5imexL&=@siV1d&pHQx zUBuF|XdD(9rD}mlpbpV8g9VIg-Y)?xg-DfCmXf05(tEl!ZIdhKUe(m0Jr^Xs8lWfl zdF~53WuREu33_IhER_`pngQa(cXP{DN;O1R6DR>%#Q?2X;6)MsLbSZ;kU*kQVgq}- z9$zB&zRCla%jGhFtzNDg0f}N$30-a682iNhJGQWO3-K@_dk(=o7%~N)f2u>}*!M2- z#<|nHrbvQ9FfFD(#Ab|Yt{n{_CYSSfg*t>C4HCgB02@t+P8>VfE0>ThV7~ly0UtUP zG-s3V7WYEz8}@4QvcW#<4uT=nTkuG;I$Wx*GiN&rXi*5HQ6d*5)A!B_Q1ewK1HlFP zH`ZpH-+2eYR-}xir>Eyh0Ppk4N2xqD!pwmPE7`V5C5q~oQP!>#k#0l6VR z_5G5FM$jx&Et?u5vv6#A!;(6@v{QKjKk3t@yzqH~_JT~MQa*~)B}{}`-)>dT*8dgz zOhzW~$AG|pK3OiTvql^#t2I)#RPPfbBPB>6QC3&Nk=66c%KIu0EEeWd%v`obWG9#9 zdLe@JI#$m%fnFnWqRIj`nT{lw5`pVoof1R1cv_)Ch9R6NK3j&D+`Oa#X;M*w<1t7~ z;PaZr+2t&Vd|Re$tS8h56ALmA>}kDN4R7}hlcWk6rbbSC4(B&Bg<-gR7{?@x4dy`+ z)ViTcidpLvkd&&9L`$HNJs6$TAg--UM89jlWRrcOQmK^N+uNsz=mT}aS+cdW1Ykh* zXo<1bDTn({F(lgLBhlB8(L6G3VH7(^*m@nKu%WcDL|E*y)3iQQUJ@cb{0Pn%*Py-# z((%k`pS?52w41EIvCeNriu2!Z(so);eCmNNjq3Bc|Va`sr% zi#y$D9#`JVHmRDsv!`q%6O*WnMFyYm@9%%Ct^otU*x1-l+uPeI3#B8W3`H0;fq^)$ zrxeXBGP02k!pzQ%wJ5sx`4p?&c_C9WC?L&@P@_`~;RA)y`iYivD0O1y+ha&Kab?VWbZ9}0Y;K+ESbth z_GI!fa3ZXz^NLV3vo-iqe}Dg9c$={)7C%%fl^-o$ym$=}U0w&4huJLT>RE-x%8nsM zb68It+W~LnW^`Ma!jm1FT`#VQV5YB}? zb5?)>aPEUTgf-|xx@bfpI8@YiQ=6C^Cs6Ov=oCNC0gz|3HbRMQorqI%9hSg*D|+>Yk4F&s4!`XMOK?S3oXU?8q&`p2bhG;wAHVI75o;|NfQ?9a+ha_?=MNuzB#`_x1IC zDrKWh^^UGerSd>WM~4ijLym2P@DT}X>Jfe1n`|QhcjdK&X{y7`J?)(5f}0%SWvjy@ zQ`&&j*&}D{3aSXnh5;(*lw`!#H4s=BChNeSq?e}Pz%9HZ8;el+ySRZt%~!3FmeM6L zszn|H)BplZDGakwBlE%ez(^>Dw##6}{^yO(r7%T^h3y_#8|<&zXM8E;c^fw1ESzp{ zZ$HD#R}xXnJV+Q6Ayfn>ZXMvsGoF~?r?|2~38xO%yN~JtDHulx__V%N-ytvcrK(gXp_3FEgjuAqHD0nuNSJP!>|3Eo_bL7#M~B#y$SstMcx=vkL_XJuU}?d{3QBcq-! zc6d1Ld$Gy~dEs{?6%6ngbC{Hg=+FE6`~Rd;sg&nqldoysW^ZrrCz<(+0M^)CFmcj0 z8ZBpwNC5Q{&h?2zmzvfjdSrql0hyQ1dh zsmygIyM`LL{{b@YjeHSNRT5|MJY|F-Wj$o_b5h%Luwn1Qqp278;#Va>ll7LFJM1AMQFF5)kwnz>a9sE+K3p%t0l%{K~?HF(H)*J2Ou;cB(|V5wC4`@X)u zAH7!FJ2w-rwK2b!nJ)+MzFL3r8UU*k1#j0#69y9)MkL=&DOkpC5YJ~_457VlUR%dl z8}S0E*QlXfmWYrt*(IQ7#oUx1 # example: http://atticd..svc.cluster.local:8080 +ATTIC_CACHE: ci # name however you want, just needs to exist +ATTIC_TOKEN: +``` + +... makes your jobs automatically cache their Nix store paths to the in-cluster +attic when running with this runner. + +Other runners could use cachix or no cache, you get the idea ;P + +!!! note + + This of course works with any executor where you can set environment + variables. This is just an example how to do it in Kubernetes easily. diff --git a/docs/multi_pipeline.md b/docs/multi_pipeline.md new file mode 100644 index 0000000..d8dbfe6 --- /dev/null +++ b/docs/multi_pipeline.md @@ -0,0 +1,31 @@ +# Multiple Pipelines + +With V2, Nix GitLab CI can generate different pipelines, depending on the +pipeline source (`$CI_PIPELINE_SOURCE`). + +By default, no matter which source, the `default` pipeline is built and ran. +`$NIX_CI_PIPELINE` can override that, eg. when manually triggering a run. +To configure which source should be 1-to-1 translated to a pipeline with the +same name, set `$NIX_CI_DEFAULT_SOURCES` to a regex which explicitly does not +match these sources. Or set it to an impossible to match regex, then it will +always run the pipeline named after `$CI_PIPELINE_SOURCE`. + +## Example 1: always run default + +If you only have a single pipeline, you just have to call it `default`. +Everything else works out of the box. + +## Example 2: default and merge_request_event + +If you want the source `merge_request_event` to trigger a different pipeline, +name it like that and set `$NIX_CI_DEFAULT_SOURCES` to `^(merge_request_event)$`. +Now a merge request will run this pipeline, while everything else runs `default`. + +## Example 3: default, push and web + +Set `$NIX_CI_DEFAULT_SOURCES` to `^(push|web)$`. + +## Example 4: always run the specific pipelines, never default + +Set `$NIX_CI_DEFAULT_SOURCES` to any regex that never matches the sources, +like `a\A` or `nothing`. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..d3aa96e --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,75 @@ +# Setup + +To integrate Nix GitLab CI into your project, you need to make two main changes: + +1. Add the `nix-gitlab-ci` flake module to your `flake.nix`. +1. Include the necessary component in your `.gitlab-ci.yml`. + +## Adding to `flake.nix` + +In your project's `flake.nix`, add `nix-gitlab-ci` as an input and import its +flake module within your `flake-parts` configuration. + +```nix title="flake.nix" +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Or your preferred nixpkgs branch/version + flake-parts.url = "github:hercules-ci/flake-parts"; + + # Add nix-gitlab-ci as an input + # recommendation: pin to a specific release/version + nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; + }; + + outputs = { nixpkgs, flake-parts, ... }@inputs: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + # Import the nix-gitlab-ci flake module + inputs.nix-gitlab-ci.flakeModule + ]; + + systems = [ + "x86_64-linux" + "aarch64-linux" + # Add other systems you need + ]; + + perSystem = { pkgs, ... }: { + # define your CI pipelines + # ci = { ... }; + # pipelines."merge_request_event" = { ... }; + }; + }; +} +``` + +Replace `` with the specific version or commit hash of `nix-gitlab-ci` +you wish to use. Pinning to a specific version is highly recommended for +reproducibility and compatibility. + +!!! warning + + While the flake input is locked through `flake.lock`, the CI/CD component + will always use the latest commit of the reference. This means that by using + a branch like `main` as version for both, the CI/CD component will always use + the latest commit while your flake uses a fixed one. + This could result in drift between both, potentially breaking stuff. + +## Including in `.gitlab-ci.yml` + +Your `.gitlab-ci.yml` file will be minimal. Its primary role is to include the +`nix-gitlab-ci` component, which will then generate the full CI configuration +based on your Nix code. + +```yaml title=".gitlab-ci.yml" +include: + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ + inputs: + # This input sets the Docker image tag used for the CI jobs. + # Use the same version as you pinned in your flake.nix for consistency. + version: +``` + +Again, ensure `` matches the version used in your `flake.nix`. +This component includes a job (`build:nix-ci`) that will evaluate your Nix +configuration and generate the `.gitlab-ci.yml` used for the pipeline run. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..b28a755 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,33 @@ +# Usage + +To create a basic pipeline, configure it by setting `ci` in `perSystem`. +The schema is similar to the `.gitlab-ci.yml`, only jobs are defined differently: + +```nix +ci = { + # Nix GitLab CI specific config, see `configType` in `flakeModule.nix` + config = {}; + jobs = { + "job-a" = {}; + "job-b" = {}; + }; +}; +``` + +For every job, there are a couple of settings you can adjust aswell: + +```nix +"job-a" = { + # see `jobType` in `flakeModule.nix` + nix = { + enable = true; # is this a nix-based job? + deps = []; # dependencies to install for this job + # for gitlab runner cache: + enable-runner-cache = false; + runner-cache-key = ""; + }; +}; +``` + +Since V2 multiple pipelines are supported. +See [Multiple Pipelines](./multi_pipeline.md) for more. diff --git a/docs/utilities.md b/docs/utilities.md new file mode 100644 index 0000000..e5a7b96 --- /dev/null +++ b/docs/utilities.md @@ -0,0 +1,59 @@ +# Utilities + +Nix GitLab CI provides a couple of utilities to help with development and +debugging. + +## Disabling caching temporarily + +Nix GitLab CI often utilizes caching mechanisms to speed up your pipelines +(see [Caching](./caching.md)). +However, there might be situations where you need to temporarily disable these +caches for a specific pipeline run, for example, to debug a caching issue or +ensure a clean build. + +To disable most of the provided caches for a pipeline, set the environment +variable `NIX_CI_DISABLE_CACHE` to any non-empty value (e.g., `yes`, `true`, `1`) +when triggering the pipeline in the GitLab UI or via the API. + +## Forcing a rebuild of the CI pipeline definition + +The job responsible for generating the `.gitlab-ci.yml` from your Nix code +(`build:nix-ci`) might itself be cached. If you've made changes to your Nix CI +configuration and the pipeline doesn't seem to pick them up, the cached job +definition might be the reason. + +You should first double check if all the Nix files you defined the CI config in +are specified in the `cache_files` CI/CD-component input +(see [CI/CD Component](./cicd_component.md) for more). + +To force this specific job to rebuild and re-evaluate your Nix configuration, +set the environment variable `NIX_CI_FORCE_BUILD` when triggering the pipeline. + +## Running jobs locally + +One of the benefits of defining your CI jobs with Nix is the ability to run them +locally in an environment that closely mirrors the CI environment. This can +significantly speed up debugging and development. + +You can run the script of any defined job locally using the `nix run` command. +The syntax is: + +```sh +nix run .#gitlab-ci:pipeline::job: +``` + +Replace `` with the name of the pipeline the job belongs to +(e.g., `default` for jobs defined under the `ci` attribute) and `` +with the name of the job you want to run. + +This command will set up the environment with the specified `nix.deps` and +execute the job's `script`. + +There is also an attribute `.#gitlab-ci:pipeline::job-deps:`. +Building this derivation will generate a shell script which exports the required +environment variables for the job, such as the `PATH` including all dependencies +and any custom environment variables that contain store paths (ensuring they are +correctly resolved across different architectures). + +You can use this to inspect the environment that would be set up for a job without +running the full script. diff --git a/flake.lock b/flake.lock index 7896ed8..f1a0875 100644 --- a/flake.lock +++ b/flake.lock @@ -169,6 +169,21 @@ "type": "github" } }, + "mkdocs-material-umami": { + "locked": { + "lastModified": 1745840856, + "narHash": "sha256-1Ad1JTMQMP6YsoIKAA+SBCE15qWrYkGue9/lXOLnu9I=", + "owner": "technofab", + "repo": "mkdocs-material-umami", + "rev": "3ac9b194450f6b779c37b8d16fec640198e5cd0a", + "type": "gitlab" + }, + "original": { + "owner": "technofab", + "repo": "mkdocs-material-umami", + "type": "gitlab" + } + }, "nix": { "inputs": { "flake-compat": [ @@ -202,6 +217,23 @@ "type": "github" } }, + "nix-mkdocs": { + "locked": { + "dir": "lib", + "lastModified": 1745175318, + "narHash": "sha256-eJOTw3SK4psqBPP2hNJ4D/13/oi/FLoM0tYKoCGVFP8=", + "owner": "technofab", + "repo": "nixmkdocs", + "rev": "6d6e0139060c896ae14de4b9c82335655a384643", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "technofab", + "repo": "nixmkdocs", + "type": "gitlab" + } + }, "nixpkgs": { "locked": { "lastModified": 1733212471, @@ -301,6 +333,8 @@ "inputs": { "devenv": "devenv", "flake-parts": "flake-parts_2", + "mkdocs-material-umami": "mkdocs-material-umami", + "nix-mkdocs": "nix-mkdocs", "nixpkgs": "nixpkgs_4", "systems": "systems", "treefmt-nix": "treefmt-nix" diff --git a/flake.nix b/flake.nix index c262d6c..6753d41 100644 --- a/flake.nix +++ b/flake.nix @@ -8,6 +8,7 @@ imports = [ inputs.devenv.flakeModule inputs.treefmt-nix.flakeModule + inputs.nix-mkdocs.flakeModule ./lib/flakeModule.nix ]; systems = import systems; @@ -25,7 +26,15 @@ mdformat.enable = true; yamlfmt.enable = true; }; - settings.formatter.yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; + settings.formatter = { + yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; + mdformat.command = let + pkg = pkgs.python3.withPackages (p: [ + p.mdformat + p.mdformat-mkdocs + ]); + in "${pkg}/bin/mdformat"; + }; }; devenv.shells.default = { containers = pkgs.lib.mkForce {}; @@ -40,9 +49,97 @@ }; }; }; + doc = { + path = ./docs; + deps = pp: [ + pp.mkdocs-material + (pp.callPackage inputs.mkdocs-material-umami {}) + ]; + config = { + site_name = "Nix GitLab CI"; + repo_name = "TECHNOFAB/nix-gitlab-ci"; + repo_url = "https://gitlab.com/TECHNOFAB/nix-gitlab-ci"; + edit_uri = "edit/main/docs/"; + theme = { + name = "material"; + features = ["content.code.copy" "content.action.edit"]; + icon.repo = "simple/gitlab"; + logo = "images/logo.png"; + favicon = "images/favicon.png"; + palette = [ + { + scheme = "default"; + media = "(prefers-color-scheme: light)"; + primary = "deep orange"; + accent = "orange"; + toggle = { + icon = "material/brightness-7"; + name = "Switch to dark mode"; + }; + } + { + scheme = "slate"; + media = "(prefers-color-scheme: dark)"; + primary = "deep orange"; + accent = "orange"; + toggle = { + icon = "material/brightness-4"; + name = "Switch to light mode"; + }; + } + ]; + }; + plugins = ["search" "material-umami"]; + nav = [ + {"Introduction" = "index.md";} + {"Setup" = "setup.md";} + {"Usage" = "usage.md";} + {"CI/CD Component" = "cicd_component.md";} + {"Environment Variables" = "environment_variables.md";} + {"Caching" = "caching.md";} + {"Multiple Pipelines" = "multi_pipeline.md";} + {"Utilities" = "utilities.md";} + {"Kubernetes Runner Example" = "kubernetes_runner.md";} + {"Example Configs" = "examples.md";} + ]; + markdown_extensions = [ + { + "pymdownx.highlight".pygments_lang_class = true; + } + "pymdownx.inlinehilite" + "pymdownx.snippets" + "pymdownx.superfences" + "fenced_code" + "admonition" + ]; + extra.analytics = { + provider = "umami"; + site_id = "28f7c904-db22-4c2b-9ee4-ed42e14b6db9"; + src = "https://analytics.tf/umami"; + domains = "nix-gitlab-ci.projects.tf"; + feedback = { + title = "Was this page helpful?"; + ratings = [ + { + icon = "material/thumb-up-outline"; + name = "This page is helpful"; + data = "good"; + note = "Thanks for your feedback!"; + } + { + icon = "material/thumb-down-outline"; + name = "This page could be improved"; + data = "bad"; + note = "Thanks for your feedback! Please leave feedback by creating an issue :)"; + } + ]; + }; + }; + }; + }; # should set the "default" pipeline ci = { - stages = ["test"]; + stages = ["test" "build" "deploy"]; jobs = { "test" = { stage = "test"; @@ -73,6 +170,31 @@ "echo \"This job will not be modified to use nix\"" ]; }; + # -- actually useful jobs -- + "docs" = { + stage = "build"; + script = [ + # sh + '' + nix build .#docs:default + mkdir -p public + cp -r result/. public/ + '' + ]; + artifacts.paths = ["public"]; + }; + "pages" = { + nix.enable = false; + image = "alpine:latest"; + stage = "deploy"; + script = ["true"]; + artifacts.paths = ["public"]; + rules = [ + { + "if" = "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"; + } + ]; + }; }; }; pipelines."non-default" = { @@ -145,6 +267,8 @@ systems.url = "github:nix-systems/default-linux"; devenv.url = "github:cachix/devenv"; treefmt-nix.url = "github:numtide/treefmt-nix"; + nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; + mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami"; }; nixConfig = { From 11537ae271158319426250c77ae8866318ad5fd2 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 10 May 2025 19:45:10 +0200 Subject: [PATCH 052/102] fix(flakeModule): use bash for stdenvMinimal fixes #24 --- lib/flakeModule.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 469fc3e..1803b4f 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -17,7 +17,7 @@ preHook = ""; allowedRequisites = null; initialPath = [pkgs.coreutils pkgs.findutils]; - shell = "/bin/sh"; + shell = "/bin/bash"; extraNativeBuildInputs = []; }; From 93739ab27c2110c76ae9be1fb19864139fcae714 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 10 May 2025 19:48:20 +0200 Subject: [PATCH 053/102] tests: try testing with nixtest --- flake.lock | 18 ++++++++++++++++++ flake.nix | 20 ++++++++++++++++++++ snapshots/default.snap.json | 1 + snapshots/non-default.snap.json | 1 + 4 files changed, 40 insertions(+) create mode 100644 snapshots/default.snap.json create mode 100644 snapshots/non-default.snap.json diff --git a/flake.lock b/flake.lock index f1a0875..598eafa 100644 --- a/flake.lock +++ b/flake.lock @@ -329,6 +329,23 @@ "type": "github" } }, + "nixtest": { + "locked": { + "dir": "lib", + "lastModified": 1746388457, + "narHash": "sha256-xWAnSFxogdy47rZyGj18R9qDlLFs/PvwPpD3iAiR2Hc=", + "owner": "technofab", + "repo": "nixtest", + "rev": "0a1bbae2c30e3ba8e3b02de223e199c5dfd56572", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "technofab", + "repo": "nixtest", + "type": "gitlab" + } + }, "root": { "inputs": { "devenv": "devenv", @@ -336,6 +353,7 @@ "mkdocs-material-umami": "mkdocs-material-umami", "nix-mkdocs": "nix-mkdocs", "nixpkgs": "nixpkgs_4", + "nixtest": "nixtest", "systems": "systems", "treefmt-nix": "treefmt-nix" } diff --git a/flake.nix b/flake.nix index 6753d41..2704302 100644 --- a/flake.nix +++ b/flake.nix @@ -9,6 +9,7 @@ inputs.devenv.flakeModule inputs.treefmt-nix.flakeModule inputs.nix-mkdocs.flakeModule + inputs.nixtest.flakeModule ./lib/flakeModule.nix ]; systems = import systems; @@ -16,6 +17,7 @@ perSystem = { pkgs, config, + self', system, ... }: rec { @@ -209,6 +211,23 @@ }; }; + nixtest.suites = let + jsonFile = file: builtins.fromJSON (builtins.readFile file); + in { + "Pipeline YAMLs" = [ + { + name = "default"; + type = "snapshot"; + actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:default"; + } + { + name = "non-default"; + type = "snapshot"; + actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:non-default"; + } + ]; + }; + packages = let setupScript = pkgs.writeShellScriptBin "setup_nix_ci" (builtins.readFile ./scripts/setup_nix_ci.sh); finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" (builtins.readFile ./scripts/finalize_nix_ci.sh); @@ -269,6 +288,7 @@ treefmt-nix.url = "github:numtide/treefmt-nix"; nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami"; + nixtest.url = "gitlab:technofab/nixtest?dir=lib"; }; nixConfig = { diff --git a/snapshots/default.snap.json b/snapshots/default.snap.json new file mode 100644 index 0000000..48ae054 --- /dev/null +++ b/snapshots/default.snap.json @@ -0,0 +1 @@ +{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build","variables":{}},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test","variables":{}},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file diff --git a/snapshots/non-default.snap.json b/snapshots/non-default.snap.json new file mode 100644 index 0000000..6a613ef --- /dev/null +++ b/snapshots/non-default.snap.json @@ -0,0 +1 @@ +{"stages":["test"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:non-default:job-deps:test\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["echo Hello from another pipeline"],"stage":"test","variables":{}}} \ No newline at end of file From 8e50828a2e837a36225d531816ac4fcc040e4144 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 10 May 2025 20:00:30 +0200 Subject: [PATCH 054/102] ci: add nixtest job --- flake.nix | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2704302..dfed39b 100644 --- a/flake.nix +++ b/flake.nix @@ -141,7 +141,7 @@ }; # should set the "default" pipeline ci = { - stages = ["test" "build" "deploy"]; + stages = ["test" "nixtest" "build" "deploy"]; jobs = { "test" = { stage = "test"; @@ -197,6 +197,18 @@ } ]; }; + "nixtest" = { + stage = "nixtest"; + script = [ + # sh + "nix run .#nixtests:run -- --junit=junit.xml" + ]; + allow_failure = true; + artifacts = { + when = "always"; + reports.junit = "junit.xml"; + }; + }; }; }; pipelines."non-default" = { From dca2d724c155799e537a898cb9f948f8afae4921 Mon Sep 17 00:00:00 2001 From: technofab Date: Sun, 11 May 2025 16:19:31 +0200 Subject: [PATCH 055/102] fix(flakeModule): use default shell for stdenvMinimal for max compatibility --- lib/flakeModule.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 1803b4f..8bafe41 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -17,7 +17,6 @@ preHook = ""; allowedRequisites = null; initialPath = [pkgs.coreutils pkgs.findutils]; - shell = "/bin/bash"; extraNativeBuildInputs = []; }; From f9c009c4506975b2af422091ac47e1a715b94329 Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 31 May 2025 15:40:53 +0200 Subject: [PATCH 056/102] feat: improve running jobs locally by adding a simple sandbox --- lib/flakeModule.nix | 112 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 13 deletions(-) diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 8bafe41..3ba1e7e 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -244,19 +244,105 @@ ); in { name = "gitlab-ci:pipeline:${pipeline_name}:job:${key}"; - value = pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' - # set up deps and environment variables containing store paths - . ${jobsMappedForDeps."gitlab-ci:pipeline:${pipeline_name}: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 [])} - ''; + value = let + actualJobScript = pkgs.writeShellScript "gitlab-ci-job:${key}:raw" '' + # set up deps and environment variables containing store paths + . ${jobsMappedForDeps."gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}"} + # normal environment variables + ${variableExports} + # run before_script, script and after_script + echo -e "\e[32mRunning before_script...\e[0m" + set -x + ${lib.concatLines (job.before_script or [])} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning script...\e[0m" + set -x + ${lib.concatLines job.script} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning after_script...\e[0m" + set -x + ${lib.concatLines (job.after_script or [])} + { set +x; } 2>/dev/null + ''; + sandboxHelper = pkgs.writeShellScriptBin "gitlab-ci-job-sandbox-helper" '' + echo -e "\e[32mSetting up...\e[0m" + + actualJobScript=$1 + shift + + INCLUDE_DIRTY=false + NO_SANDBOX=false + KEEP_TMP=false + KEEP_ENV="" + # parse flags + while [[ $# -gt 0 ]]; do + case "$1" in + --include-dirty) + INCLUDE_DIRTY=true + shift + ;; + --no-sandbox) + NO_SANDBOX=true + shift + ;; + --keep-tmp) + KEEP_TMP=true + shift + ;; + --keep-env) + KEEP_ENV="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + done + + if [ "$NO_SANDBOX" = false ]; then + echo "Running with simple sandboxing" + if [ "$KEEP_TMP" = false ]; then + trap "rm -rf '$TMPDIR'" EXIT + else + echo "Temp dir will be preserved at: $TMPDIR" + fi + + # check if dirty + DIRTY_PATCH="" + if ! git diff --quiet && ! git diff --staged --quiet; then + echo "Warning: working tree is dirty." + DIRTY_PATCH=$(mktemp -t "nix-gitlab-ci.XXX.patch") + git diff --staged > "$DIRTY_PATCH" + trap "rm -f '$DIRTY_PATCH'" EXIT + fi + TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") + git clone . $TMPDIR + pushd $TMPDIR >/dev/null + if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then + echo "Copying dirty changes..." + git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" + fi + + echo "Running job in $TMPDIR" + env -i $( + if [[ -n "$KEEP_ENV" ]]; then + IFS=',' read -ra VARS <<< "$KEEP_ENV" + for var in "''${VARS[@]}"; do + printf '%s=%q ' "$var" "''${!var}" + done + fi + ) bash $actualJobScript + popd >/dev/null + else + exec $actualJobScript + fi + ''; + in + # this way the sandbox helper just needs to be built once + pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' + exec ${lib.getExe sandboxHelper} ${actualJobScript} $@ + ''; }) jobs; # build the deps specific for this job before anything, this way the deps should be fetched from the cache From 2f197d2c50343c805b6859c04e46624e07d6847d Mon Sep 17 00:00:00 2001 From: technofab Date: Sat, 31 May 2025 21:29:54 +0200 Subject: [PATCH 057/102] chore: split everything up into their own files & add a bunch of tests --- .gitignore | 1 + flake.lock | 6 +- flake.nix | 23 +-- lib/default.nix | 8 + lib/flake.nix | 11 +- lib/flakeModule.nix | 249 ++------------------------------ lib/helpers.nix | 49 +++++++ lib/jobDeps.nix | 36 +++++ lib/jobPatch.nix | 49 +++++++ lib/jobRun.nix | 48 ++++++ lib/pipeline.nix | 61 ++++++++ lib/sandbox_helper.sh | 73 ++++++++++ lib/utils.nix | 90 ++++++------ snapshots/default.snap.json | 2 +- snapshots/non-default.snap.json | 2 +- tests/ci-lib.nix | 140 ++++++++++++++++++ tests/default.nix | 8 + tests/helpers.nix | 96 ++++++++++++ tests/pipeline-yamls.nix | 26 ++++ tests/utils.nix | 36 +++++ 20 files changed, 704 insertions(+), 310 deletions(-) create mode 100644 lib/default.nix create mode 100644 lib/helpers.nix create mode 100644 lib/jobDeps.nix create mode 100644 lib/jobPatch.nix create mode 100644 lib/jobRun.nix create mode 100644 lib/pipeline.nix create mode 100644 lib/sandbox_helper.sh create mode 100644 tests/ci-lib.nix create mode 100644 tests/default.nix create mode 100644 tests/helpers.nix create mode 100644 tests/pipeline-yamls.nix create mode 100644 tests/utils.nix diff --git a/.gitignore b/.gitignore index 2a8dde4..bbee8ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .direnv .pre-commit-config.yaml result +*.xml diff --git a/flake.lock b/flake.lock index 598eafa..56dfb71 100644 --- a/flake.lock +++ b/flake.lock @@ -332,11 +332,11 @@ "nixtest": { "locked": { "dir": "lib", - "lastModified": 1746388457, - "narHash": "sha256-xWAnSFxogdy47rZyGj18R9qDlLFs/PvwPpD3iAiR2Hc=", + "lastModified": 1748711200, + "narHash": "sha256-1Fx0jDk4ZsLkX3oSHo14OwzsK3sHkc2ykfz4bYqWuGA=", "owner": "technofab", "repo": "nixtest", - "rev": "0a1bbae2c30e3ba8e3b02de223e199c5dfd56572", + "rev": "3ff5b358d506473ace2311cd25fb95be0c048997", "type": "gitlab" }, "original": { diff --git a/flake.nix b/flake.nix index dfed39b..e4e363f 100644 --- a/flake.nix +++ b/flake.nix @@ -15,12 +15,16 @@ systems = import systems; flake = {}; perSystem = { + lib, pkgs, config, self', system, ... }: rec { + imports = [ + ./tests + ]; treefmt = { projectRootFile = "flake.nix"; programs = { @@ -201,7 +205,7 @@ stage = "nixtest"; script = [ # sh - "nix run .#nixtests:run -- --junit=junit.xml" + "nix run .#nixtests:run -- --junit=junit.xml --pure" ]; allow_failure = true; artifacts = { @@ -223,23 +227,6 @@ }; }; - nixtest.suites = let - jsonFile = file: builtins.fromJSON (builtins.readFile file); - in { - "Pipeline YAMLs" = [ - { - name = "default"; - type = "snapshot"; - actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:default"; - } - { - name = "non-default"; - type = "snapshot"; - actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:non-default"; - } - ]; - }; - packages = let setupScript = pkgs.writeShellScriptBin "setup_nix_ci" (builtins.readFile ./scripts/setup_nix_ci.sh); finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" (builtins.readFile ./scripts/finalize_nix_ci.sh); diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..37f6318 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,8 @@ +args: { + helpers = import ./helpers.nix args; + jobDeps = import ./jobDeps.nix args; + jobRun = import ./jobRun.nix args; + jobPatch = import ./jobPatch.nix args; + pipeline = import ./pipeline.nix args; + utils = import ./utils.nix args; +} diff --git a/lib/flake.nix b/lib/flake.nix index 5d00bb6..b2cc77a 100644 --- a/lib/flake.nix +++ b/lib/flake.nix @@ -1,9 +1,8 @@ { - description = "Nix-CI lib"; + description = "Nix-GitLab-CI lib"; - outputs = {...} @ inputs: - { - flakeModule = import ./flakeModule.nix; - } - // (import ./utils.nix); + outputs = {...}: { + flakeModule = import ./flakeModule.nix; + lib = import ./default.nix; + }; } diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix index 3ba1e7e..4f8a274 100644 --- a/lib/flakeModule.nix +++ b/lib/flakeModule.nix @@ -9,22 +9,12 @@ pkgs, ... }: let - inherit (lib) isAttrs filterAttrs mapAttrs types mkOption toList; + cilib = import ./. {inherit lib pkgs;}; + inherit (cilib.pipeline) mkPipeline; + inherit (lib) types mkOption; + cfg = config.ci.config; - stdenvMinimal = pkgs.stdenvNoCC.override { - cc = null; - preHook = ""; - allowedRequisites = null; - initialPath = [pkgs.coreutils pkgs.findutils]; - extraNativeBuildInputs = []; - }; - - filterAttrsRec = pred: v: - if isAttrs v - then filterAttrs pred (mapAttrs (path: filterAttrsRec pred) v) - else v; - subType = options: types.submodule {inherit options;}; mkNullOption = type: mkOption { @@ -168,228 +158,15 @@ }; }; - config.legacyPackages = let - # NOTE: json is also valid yaml and this removes dependency on jq - # and/or remarshal (used in pkgs.formats.json and pkgs.formats.yaml - # respectively) - toYaml = name: value: builtins.toFile name (builtins.toJSON value); - 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 {}); - in - lib.fold (pipeline: acc: acc // pipeline) {} (map ( - pipeline_name: let - pipeline = config.pipelines."${pipeline_name}"; - jobs = filterAttrsRec (n: v: v != null) pipeline.jobs; - rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs pipeline ["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:pipeline:${pipeline_name}:job-deps:${key}"; - value = stdenvMinimal.mkDerivation { - name = "gitlab-ci-job-deps-${key}"; - dontUnpack = true; - installPhase = let - script = '' - export PATH="${lib.makeBinPath job.nix.deps}:$PATH"; - # variables containing nix derivations: - ${variableExports} - ''; - in - # sh - '' - echo '${script}' > $out - chmod +x $out - ''; - }; - }) - jobs; - # allows the user to directly run the script - jobsMappedForScript = - mapAttrs (key: job: let - variablesWithoutStorePaths = filterJobVariables false job; - variableExports = lib.concatLines ( - lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithoutStorePaths - ); - in { - name = "gitlab-ci:pipeline:${pipeline_name}:job:${key}"; - value = let - actualJobScript = pkgs.writeShellScript "gitlab-ci-job:${key}:raw" '' - # set up deps and environment variables containing store paths - . ${jobsMappedForDeps."gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}"} - # normal environment variables - ${variableExports} - # run before_script, script and after_script - echo -e "\e[32mRunning before_script...\e[0m" - set -x - ${lib.concatLines (job.before_script or [])} - { set +x; } 2>/dev/null - echo -e "\e[32mRunning script...\e[0m" - set -x - ${lib.concatLines job.script} - { set +x; } 2>/dev/null - echo -e "\e[32mRunning after_script...\e[0m" - set -x - ${lib.concatLines (job.after_script or [])} - { set +x; } 2>/dev/null - ''; - sandboxHelper = pkgs.writeShellScriptBin "gitlab-ci-job-sandbox-helper" '' - echo -e "\e[32mSetting up...\e[0m" - - actualJobScript=$1 - shift - - INCLUDE_DIRTY=false - NO_SANDBOX=false - KEEP_TMP=false - KEEP_ENV="" - # parse flags - while [[ $# -gt 0 ]]; do - case "$1" in - --include-dirty) - INCLUDE_DIRTY=true - shift - ;; - --no-sandbox) - NO_SANDBOX=true - shift - ;; - --keep-tmp) - KEEP_TMP=true - shift - ;; - --keep-env) - KEEP_ENV="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac - done - - if [ "$NO_SANDBOX" = false ]; then - echo "Running with simple sandboxing" - if [ "$KEEP_TMP" = false ]; then - trap "rm -rf '$TMPDIR'" EXIT - else - echo "Temp dir will be preserved at: $TMPDIR" - fi - - # check if dirty - DIRTY_PATCH="" - if ! git diff --quiet && ! git diff --staged --quiet; then - echo "Warning: working tree is dirty." - DIRTY_PATCH=$(mktemp -t "nix-gitlab-ci.XXX.patch") - git diff --staged > "$DIRTY_PATCH" - trap "rm -f '$DIRTY_PATCH'" EXIT - fi - TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") - git clone . $TMPDIR - pushd $TMPDIR >/dev/null - if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then - echo "Copying dirty changes..." - git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" - fi - - echo "Running job in $TMPDIR" - env -i $( - if [[ -n "$KEEP_ENV" ]]; then - IFS=',' read -ra VARS <<< "$KEEP_ENV" - for var in "''${VARS[@]}"; do - printf '%s=%q ' "$var" "''${!var}" - done - fi - ) bash $actualJobScript - popd >/dev/null - else - exec $actualJobScript - fi - ''; - in - # this way the sandbox helper just needs to be built once - pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' - exec ${lib.getExe sandboxHelper} ${actualJobScript} $@ - ''; - }) - 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 \"gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}\"" - ] - (appendToAfterScript [ - "finalize_nix_ci" - ] - job)) - // lib.optionalAttrs job.nix.enable { - image = job.image; - variables = - (filterJobVariables false job) - // lib.optionalAttrs job.nix.enable-runner-cache { - NIX_CI_CACHE_STRATEGY = "runner"; - }; - cache = - ( - let - c = job.cache or []; - in - toList c - ) - ++ (lib.optional (job.nix.enable-runner-cache) { - key = job.nix.runner-cache-key; - paths = [".nix-cache/"]; - }); - } - ) ["nix"]; - }) - jobs; - in - # gitlab-ci:pipeline: - # gitlab-ci:pipeline::job: - # gitlab-ci:pipeline::job-deps: - { - "gitlab-ci:pipeline:${pipeline_name}" = toYaml "gitlab-ci-${pipeline_name}.yml" (rest // jobsPatched); - } - // jobsMappedForDeps - // jobsMappedForScript - ) (builtins.attrNames config.pipelines)); + config.legacyPackages = lib.fold (pipeline: acc: acc // pipeline) {} ( + map ( + pipeline_name: + (mkPipeline { + pipeline = config.pipelines."${pipeline_name}"; + name = pipeline_name; + }).packages + ) (builtins.attrNames config.pipelines) + ); } ); } diff --git a/lib/helpers.nix b/lib/helpers.nix new file mode 100644 index 0000000..3b4c057 --- /dev/null +++ b/lib/helpers.nix @@ -0,0 +1,49 @@ +{lib, ...} @ args: let + inherit (lib) isAttrs filterAttrs mapAttrs; +in rec { + prepend = key: arr: job: + job + // lib.optionalAttrs (job.nix.enable or false) { + ${key} = + arr + ++ (job.${key} or []); + }; + append = key: arr: job: + job + // lib.optionalAttrs (job.nix.enable or false) { + ${key} = (job.${key} or []) ++ arr; + }; + prependToBeforeScript = prepend "before_script"; + appendToAfterScript = append "after_script"; + + # json is also valid yaml and this removes dependency on jq and/or remarshal + # (used in pkgs.formats.json and pkgs.formats.yaml respectively) + toYaml = name: value: builtins.toFile name (builtins.toJSON value); + + customMapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); + + filterAttrsRec = pred: v: + if isAttrs v + then filterAttrs pred (mapAttrs (path: filterAttrsRec pred) v) + else v; + + # 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 {}); + + # args.pkgs so "pkgs" does not need to be passed all the time + stdenvMinimal = args.pkgs.stdenvNoCC.override { + cc = null; + preHook = ""; + allowedRequisites = null; + initialPath = with args.pkgs; [coreutils findutils]; + extraNativeBuildInputs = []; + }; +} diff --git a/lib/jobDeps.nix b/lib/jobDeps.nix new file mode 100644 index 0000000..27bad19 --- /dev/null +++ b/lib/jobDeps.nix @@ -0,0 +1,36 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./. {inherit lib pkgs;}; + inherit (cilib.helpers) filterJobVariables stdenvMinimal; +in { + mkJobDeps = { + key, + job, + }: let + variablesWithStorePaths = filterJobVariables true job; + variableExports = lib.concatLines ( + lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths + ); + script = '' + export PATH="${lib.makeBinPath (job.nix.deps or [])}:$PATH"; + # variables containing nix derivations: + ${variableExports} + ''; + in + stdenvMinimal.mkDerivation { + name = "gitlab-ci-job-deps-${key}"; + dontUnpack = true; + installPhase = + # sh + '' + echo '${script}' > $out + chmod +x $out + ''; + passthru = { + inherit script; + }; + }; +} diff --git a/lib/jobPatch.nix b/lib/jobPatch.nix new file mode 100644 index 0000000..959a9c0 --- /dev/null +++ b/lib/jobPatch.nix @@ -0,0 +1,49 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./. {inherit lib pkgs;}; + inherit (lib) toList; + inherit (cilib.helpers) prependToBeforeScript appendToAfterScript filterJobVariables; +in { + mkJobPatched = { + key, + job, + pipeline_name, + }: + builtins.removeAttrs ( + (prependToBeforeScript [ + "source setup_nix_ci \"gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}\"" + ] + (appendToAfterScript [ + "finalize_nix_ci" + ] + job)) + // lib.optionalAttrs job.nix.enable ( + (let + variables = + (filterJobVariables false job) + // lib.optionalAttrs job.nix.enable-runner-cache { + NIX_CI_CACHE_STRATEGY = "runner"; + }; + in + # filter empty variables + lib.optionalAttrs (variables != {}) { + inherit variables; + }) + // (let + cache = + (toList (job.cache or [])) + ++ (lib.optional (job.nix.enable-runner-cache) { + key = job.nix.runner-cache-key; + paths = [".nix-cache/"]; + }); + in + # filter empty cache + lib.optionalAttrs (cache != []) { + inherit cache; + }) + ) + ) ["nix"]; +} diff --git a/lib/jobRun.nix b/lib/jobRun.nix new file mode 100644 index 0000000..5c4a118 --- /dev/null +++ b/lib/jobRun.nix @@ -0,0 +1,48 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./. {inherit lib pkgs;}; + inherit (cilib.helpers) filterJobVariables; +in { + mkJobRun = { + key, + job, + jobDeps, + }: let + variablesWithoutStorePaths = filterJobVariables false job; + variableExports = lib.concatLines ( + lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithoutStorePaths + ); + sandboxHelper = pkgs.writeShellScriptBin "gitlab-ci-job-sandbox-helper" (builtins.readFile ./sandbox_helper.sh); + actualJobScript = pkgs.writeShellScript "gitlab-ci-job:${key}:raw" '' + # set up deps and environment variables containing store paths + . ${jobDeps} + # normal environment variables + ${variableExports} + # run before_script, script and after_script + echo -e "\e[32mRunning before_script...\e[0m" + set -x + ${lib.concatLines (job.before_script or [])} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning script...\e[0m" + set -x + ${lib.concatLines job.script} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning after_script...\e[0m" + set -x + ${lib.concatLines (job.after_script or [])} + { set +x; } 2>/dev/null + ''; + in + # this way the sandbox helper just needs to be built once + pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' + exec ${lib.getExe sandboxHelper} ${actualJobScript} $@ + '' + // { + passthru = { + inherit jobDeps actualJobScript; + }; + }; +} diff --git a/lib/pipeline.nix b/lib/pipeline.nix new file mode 100644 index 0000000..2873b22 --- /dev/null +++ b/lib/pipeline.nix @@ -0,0 +1,61 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./. {inherit lib pkgs;}; + inherit (cilib.helpers) filterAttrsRec customMapAttrs toYaml; + inherit (cilib.jobDeps) mkJobDeps; + inherit (cilib.jobRun) mkJobRun; + inherit (cilib.jobPatch) mkJobPatched; +in { + mkPipeline = { + name, + pipeline, + }: let + jobs = filterAttrsRec (n: v: v != null) pipeline.jobs; + rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs pipeline ["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 = + customMapAttrs (key: job: { + name = "gitlab-ci:pipeline:${name}:job-deps:${key}"; + value = mkJobDeps {inherit key job;}; + }) + jobs; + # allows the user to directly run the script + jobsMappedForScript = + customMapAttrs (key: job: { + name = "gitlab-ci:pipeline:${name}:job:${key}"; + value = mkJobRun { + inherit key job; + jobDeps = jobsMappedForDeps."gitlab-ci:pipeline:${name}:job-deps:${key}"; + }; + }) + jobs; + # build the deps specific for this job before anything, this way the deps should be fetched from the cache + jobsPatched = + customMapAttrs (key: job: { + name = key; + value = assert lib.assertMsg (builtins.elem job.stage (rest.stages or [])) "stage '${job.stage}' of job '${key}' does not exist"; + mkJobPatched { + inherit key job; + pipeline_name = name; + }; + }) + jobs; + in { + packages = + # gitlab-ci:pipeline: + # gitlab-ci:pipeline::job: + # gitlab-ci:pipeline::job-deps: + { + "gitlab-ci:pipeline:${name}" = toYaml "gitlab-ci-${name}.yml" (rest // jobsPatched); + } + // jobsMappedForDeps + // jobsMappedForScript; + finalConfig = rest // jobsPatched; + }; +} diff --git a/lib/sandbox_helper.sh b/lib/sandbox_helper.sh new file mode 100644 index 0000000..df69d97 --- /dev/null +++ b/lib/sandbox_helper.sh @@ -0,0 +1,73 @@ +echo -e "\e[32mSetting up...\e[0m" + +actualJobScript=$1 +shift + +INCLUDE_DIRTY=false +NO_SANDBOX=false +KEEP_TMP=false +KEEP_ENV="" +# parse flags +while [[ $# -gt 0 ]]; do + case "$1" in + --include-dirty) + INCLUDE_DIRTY=true + shift + ;; + --no-sandbox) + NO_SANDBOX=true + shift + ;; + --keep-tmp) + KEEP_TMP=true + shift + ;; + --keep-env) + KEEP_ENV="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [ "$NO_SANDBOX" = false ]; then + echo "Running with simple sandboxing" + if [ "$KEEP_TMP" = false ]; then + trap "rm -rf '$TMPDIR'" EXIT + else + echo "Temp dir will be preserved at: $TMPDIR" + fi + + # check if dirty + DIRTY_PATCH="" + if ! git diff --quiet && ! git diff --staged --quiet; then + echo "Warning: working tree is dirty." + DIRTY_PATCH=$(mktemp -t "nix-gitlab-ci.XXX.patch") + git diff --staged > "$DIRTY_PATCH" + trap "rm -f '$DIRTY_PATCH'" EXIT + fi + TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") + git clone . $TMPDIR + pushd $TMPDIR >/dev/null + if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then + echo "Copying dirty changes..." + git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" + fi + + echo "Running job in $TMPDIR" + env -i $( + if [[ -n "$KEEP_ENV" ]]; then + IFS=',' read -ra VARS <<< "$KEEP_ENV" + for var in "''${VARS[@]}"; do + printf '%s=%q ' "$var" "''${!var}" + done + fi + ) bash $actualJobScript + popd >/dev/null +else + exec $actualJobScript +fi + diff --git a/lib/utils.nix b/lib/utils.nix index a40dadd..ae15284 100644 --- a/lib/utils.nix +++ b/lib/utils.nix @@ -1,46 +1,46 @@ -{ - mkUtils = {pkgs, ...}: { - commitAndPushFiles = { - message, - files ? [], - }: jobArgs: - jobArgs - // { - before_script = - (jobArgs.before_script or []) - ++ [ - '' - echo -e "\\e[0Ksection_start:`date +%s`:commit_setup[collapsed=true]\\r\\e[0KSetting up commitAndPushFiles" - eval "$(ssh-agent -s)" >/dev/null; - mkdir -p ~/.ssh; touch ~/.ssh/known_hosts; - ssh-keyscan -t rsa $CI_SERVER_HOST >> ~/.ssh/known_hosts; - echo "$GIT_SSH_PRIV_KEY" | tr -d '\r' | ssh-add - >/dev/null; - git config --global user.email "$GIT_EMAIL" >/dev/null; - git config --global user.name "$GIT_NAME" >/dev/null; - export CI_PUSH_REPO=`echo $CI_REPOSITORY_URL | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:|"`; - git remote rm origin && git remote add origin ''${CI_PUSH_REPO} - echo -e "\\e[0Ksection_end:`date +%s`:commit_setup\\r\\e[0K" - '' - ]; - script = let - addScript = - if builtins.length files == 0 - then "" - else "git add ${builtins.concatStringsSep " " files}"; - in - (jobArgs.script or []) - ++ [ - '' - echo -e "\\e[0Ksection_start:`date +%s`:commit[collapsed=true]\\r\\e[0KCommiting & pushing changes if necessary" - ${addScript} - git diff --cached --exit-code >/dev/null && - echo "Nothing to commit" || - git commit -m "${message}" --no-verify; - git push --tags origin ''${GIT_SOURCE_REF:-HEAD}:''${GIT_TARGET_REF:-$CI_COMMIT_REF_NAME} -o ci.skip - echo -e "\\e[0Ksection_end:`date +%s`:commit\\r\\e[0K" - '' - ]; - nix.deps = (jobArgs.nix.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; - }; - }; +{pkgs, ...}: { + commitAndPushFiles = { + message, + files ? [], + }: jobArgs: + jobArgs + // { + before_script = + (jobArgs.before_script or []) + ++ [ + # sh + '' + echo -e "\\e[0Ksection_start:`date +%s`:commit_setup[collapsed=true]\\r\\e[0KSetting up commitAndPushFiles" + eval "$(ssh-agent -s)" >/dev/null; + mkdir -p ~/.ssh; touch ~/.ssh/known_hosts; + ssh-keyscan -t rsa $CI_SERVER_HOST >> ~/.ssh/known_hosts; + echo "$GIT_SSH_PRIV_KEY" | tr -d '\r' | ssh-add - >/dev/null; + git config --global user.email "$GIT_EMAIL" >/dev/null; + git config --global user.name "$GIT_NAME" >/dev/null; + export CI_PUSH_REPO=`echo $CI_REPOSITORY_URL | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:|"`; + git remote rm origin && git remote add origin ''${CI_PUSH_REPO} + echo -e "\\e[0Ksection_end:`date +%s`:commit_setup\\r\\e[0K" + '' + ]; + script = let + addScript = + if builtins.length files == 0 + then "" + else "git add ${builtins.concatStringsSep " " files}"; + in + (jobArgs.script or []) + ++ [ + # sh + '' + echo -e "\\e[0Ksection_start:`date +%s`:commit[collapsed=true]\\r\\e[0KCommiting & pushing changes if necessary" + ${addScript} + git diff --cached --exit-code >/dev/null && + echo "Nothing to commit" || + git commit -m "${message}" --no-verify; + git push --tags origin ''${GIT_SOURCE_REF:-HEAD}:''${GIT_TARGET_REF:-$CI_COMMIT_REF_NAME} -o ci.skip + echo -e "\\e[0Ksection_end:`date +%s`:commit\\r\\e[0K" + '' + ]; + nix.deps = (jobArgs.nix.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; + }; } diff --git a/snapshots/default.snap.json b/snapshots/default.snap.json index 48ae054..1b9723c 100644 --- a/snapshots/default.snap.json +++ b/snapshots/default.snap.json @@ -1 +1 @@ -{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build","variables":{}},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test","variables":{}},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file +{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build"},"nixtest":{"after_script":["finalize_nix_ci"],"allow_failure":true,"artifacts":{"reports":{"junit":"junit.xml"},"when":"always"},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:nixtest\""],"image":"$NIX_CI_IMAGE","script":["nix run .#nixtests:run -- --junit=junit.xml"],"stage":"nixtest"},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","nixtest","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test"},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file diff --git a/snapshots/non-default.snap.json b/snapshots/non-default.snap.json index 6a613ef..9ed3256 100644 --- a/snapshots/non-default.snap.json +++ b/snapshots/non-default.snap.json @@ -1 +1 @@ -{"stages":["test"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:non-default:job-deps:test\""],"cache":[],"image":"$NIX_CI_IMAGE","script":["echo Hello from another pipeline"],"stage":"test","variables":{}}} \ No newline at end of file +{"stages":["test"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:non-default:job-deps:test\""],"image":"$NIX_CI_IMAGE","script":["echo Hello from another pipeline"],"stage":"test"}} \ No newline at end of file diff --git a/tests/ci-lib.nix b/tests/ci-lib.nix new file mode 100644 index 0000000..577139c --- /dev/null +++ b/tests/ci-lib.nix @@ -0,0 +1,140 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./../lib {inherit lib pkgs;}; +in { + nixtest.suites."CI Lib" = { + pos = __curPos; + tests = let + inherit (cilib.jobDeps) mkJobDeps; + inherit (cilib.jobRun) mkJobRun; + inherit (cilib.jobPatch) mkJobPatched; + inherit (cilib.pipeline) mkPipeline; + deps = mkJobDeps { + key = "test"; + job = { + nix.deps = [pkgs.hello]; + variables.TEST = "${pkgs.curl}"; + }; + }; + in [ + { + name = "jobDeps"; + type = "script"; + script = + # sh + '' + export PATH=${lib.makeBinPath [pkgs.gnugrep]} + grep -q "/nix/store" ${deps} + grep -q 'hello/bin:$PATH' ${deps} + grep -q "export TEST=" ${deps} + grep -q "curl" ${deps} + ''; + } + { + name = "jobRun"; + type = "script"; + script = let + run = mkJobRun { + key = "test"; + job.script = ["hello"]; + jobDeps = deps; + }; + in + # sh + '' + export PATH=${lib.makeBinPath [pkgs.gnugrep]} + grep -q "sandbox-helper" ${run}/bin/gitlab-ci-job:test + grep -q "gitlab-ci-job-test-raw" ${run}/bin/gitlab-ci-job:test + grep -q "gitlab-ci-job-deps-test" ${run.passthru.actualJobScript} + grep -q "Running script..." ${run.passthru.actualJobScript} + grep -q "hello" ${run.passthru.actualJobScript} + ''; + } + { + name = "jobPatched nix disabled"; + expected = {}; + actual = mkJobPatched { + key = "test"; + pipeline_name = "test"; + job.nix.enable = false; + }; + } + { + name = "jobPatched without runner cache"; + expected = { + after_script = ["finalize_nix_ci"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + }; + actual = mkJobPatched { + key = "test"; + pipeline_name = "test"; + job.nix = { + enable = true; + enable-runner-cache = false; + }; + }; + } + { + name = "jobPatched with runner cache"; + expected = { + after_script = ["finalize_nix_ci"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + cache = [ + { + key = "test"; + paths = [".nix-cache/"]; + } + ]; + variables."NIX_CI_CACHE_STRATEGY" = "runner"; + }; + actual = mkJobPatched { + key = "test"; + pipeline_name = "test"; + job.nix = { + enable = true; + enable-runner-cache = true; + runner-cache-key = "test"; + }; + }; + } + { + name = "mkPipeline empty"; + expected = {}; + actual = + (mkPipeline { + name = "test"; + pipeline.jobs = {}; + }).finalConfig; + } + { + name = "mkPipeline empty packages"; + type = "script"; + script = let + pipeline = builtins.toFile "pipeline-test" (builtins.toJSON + (mkPipeline { + name = "test"; + pipeline.jobs = {}; + }).packages); + in + # sh + '' + set -euo pipefail + export PATH=${lib.makeBinPath [pkgs.jq pkgs.gnugrep pkgs.coreutils]} + # single key + jq 'keys | length == 1' "${pipeline}" | grep -q true + # key is exactly "gitlab-ci:pipeline:test" + jq -r 'keys[0]' "${pipeline}" | grep -qx "gitlab-ci:pipeline:test" + # value contains "/nix/store/" + jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "/nix/store/" + # value contains "gitlab-ci-test.yml" + jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "gitlab-ci-test.yml" + # file only contains "{}" + [[ "$(cat $(jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}"))" == "{}" ]] + ''; + } + ]; + }; +} diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 0000000..764ae46 --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,8 @@ +{ + imports = [ + ./utils.nix + ./ci-lib.nix + ./helpers.nix + ./pipeline-yamls.nix + ]; +} diff --git a/tests/helpers.nix b/tests/helpers.nix new file mode 100644 index 0000000..1d100fc --- /dev/null +++ b/tests/helpers.nix @@ -0,0 +1,96 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./../lib {inherit lib pkgs;}; +in { + nixtest.suites."Helpers" = { + pos = __curPos; + tests = let + inherit (cilib) helpers; + in [ + { + name = "appendToAfterScript nix disabled"; + expected = {}; + actual = helpers.appendToAfterScript [] {}; + } + { + name = "appendToAfterScript empty"; + expected = { + nix.enable = true; + after_script = []; + }; + actual = helpers.appendToAfterScript [] {nix.enable = true;}; + } + { + name = "appendToAfterScript"; + expected = { + nix.enable = true; + after_script = ["echo after_script" "finalize_nix_ci"]; + }; + actual = helpers.appendToAfterScript ["finalize_nix_ci"] { + nix.enable = true; + after_script = ["echo after_script"]; + }; + } + { + name = "prependToBeforeScript nix disabled"; + expected = {}; + actual = helpers.prependToBeforeScript [] {}; + } + { + name = "prependToBeforeScript empty"; + expected = { + nix.enable = true; + before_script = []; + }; + actual = helpers.prependToBeforeScript [] {nix.enable = true;}; + } + { + name = "prependToBeforeScript"; + expected = { + nix.enable = true; + before_script = ["setup_nix_ci" "echo before_script"]; + }; + actual = helpers.prependToBeforeScript ["setup_nix_ci"] { + nix.enable = true; + before_script = ["echo before_script"]; + }; + } + { + name = "toYaml"; + expected = ''{"hello":"world"}''; + actual = builtins.readFile (helpers.toYaml "test" {hello = "world";}); + } + { + name = "filterAttrsRec"; + expected = {world = "world";}; + actual = helpers.filterAttrsRec (n: v: v != null) { + hello = null; + world = "world"; + }; + } + { + name = "filterJobVariables with store paths"; + expected = {HELLO = "${pkgs.hello}";}; + actual = helpers.filterJobVariables true { + variables = { + HELLO = "${pkgs.hello}"; + WORLD = "world"; + }; + }; + } + { + name = "filterJobVariables without store paths"; + expected = {WORLD = "world";}; + actual = helpers.filterJobVariables false { + variables = { + HELLO = "${pkgs.hello}"; + WORLD = "world"; + }; + }; + } + ]; + }; +} diff --git a/tests/pipeline-yamls.nix b/tests/pipeline-yamls.nix new file mode 100644 index 0000000..32effbe --- /dev/null +++ b/tests/pipeline-yamls.nix @@ -0,0 +1,26 @@ +{ + lib, + pkgs, + self', + ... +}: let + cilib = import ./../lib {inherit lib pkgs;}; +in { + nixtest.suites."Pipeline YAMLs" = { + pos = __curPos; + tests = let + jsonFile = file: builtins.fromJSON (builtins.readFile file); + in [ + { + name = "default"; + type = "snapshot"; + actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:default"; + } + { + name = "non-default"; + type = "snapshot"; + actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:non-default"; + } + ]; + }; +} diff --git a/tests/utils.nix b/tests/utils.nix new file mode 100644 index 0000000..8d9591f --- /dev/null +++ b/tests/utils.nix @@ -0,0 +1,36 @@ +{ + lib, + pkgs, + ... +}: let + cilib = import ./../lib {inherit lib pkgs;}; +in { + nixtest.suites."Utils" = { + pos = __curPos; + tests = [ + { + name = "commitAndPushFiles"; + type = "script"; + script = let + inherit (cilib) utils; + job = builtins.toFile "test" ( + builtins.unsafeDiscardStringContext ( + builtins.toJSON ( + utils.commitAndPushFiles { + message = "hello world"; + files = ["a.md" "b.txt"]; + } {} + ) + ) + ); + in + # sh + '' + export PATH=${lib.makeBinPath [pkgs.gnugrep]} + grep -q 'git commit -m \\"hello world\\"' ${job} + grep -q 'git add a.md b.txt' ${job} + ''; + } + ]; + }; +} From 95d7a950b94909a66dcee2bc91f12ae54a9e6532 Mon Sep 17 00:00:00 2001 From: TECHNOFAB Date: Sat, 31 May 2025 23:08:35 +0200 Subject: [PATCH 058/102] ci: update test snapshots --- snapshots/default.snap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshots/default.snap.json b/snapshots/default.snap.json index 1b9723c..c247a78 100644 --- a/snapshots/default.snap.json +++ b/snapshots/default.snap.json @@ -1 +1 @@ -{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build"},"nixtest":{"after_script":["finalize_nix_ci"],"allow_failure":true,"artifacts":{"reports":{"junit":"junit.xml"},"when":"always"},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:nixtest\""],"image":"$NIX_CI_IMAGE","script":["nix run .#nixtests:run -- --junit=junit.xml"],"stage":"nixtest"},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","nixtest","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test"},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file +{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build"},"nixtest":{"after_script":["finalize_nix_ci"],"allow_failure":true,"artifacts":{"reports":{"junit":"junit.xml"},"when":"always"},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:nixtest\""],"image":"$NIX_CI_IMAGE","script":["nix run .#nixtests:run -- --junit=junit.xml --pure"],"stage":"nixtest"},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","nixtest","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test"},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file From df35d61170c5dcbb2e64fd5fdcd3d449335231e9 Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 2 Jun 2025 22:15:44 +0200 Subject: [PATCH 059/102] fix(sandbox_helper): remove '' --- lib/sandbox_helper.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sandbox_helper.sh b/lib/sandbox_helper.sh index df69d97..4ea5ec5 100644 --- a/lib/sandbox_helper.sh +++ b/lib/sandbox_helper.sh @@ -61,8 +61,8 @@ if [ "$NO_SANDBOX" = false ]; then env -i $( if [[ -n "$KEEP_ENV" ]]; then IFS=',' read -ra VARS <<< "$KEEP_ENV" - for var in "''${VARS[@]}"; do - printf '%s=%q ' "$var" "''${!var}" + for var in "${VARS[@]}"; do + printf '%s=%q ' "$var" "${!var}" done fi ) bash $actualJobScript From 69e0f7a4191112f265300e91d9ef8fa662da943a Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 3 Jun 2025 12:23:51 +0200 Subject: [PATCH 060/102] chore(flake): update nixtest --- flake.lock | 7 ++++--- flake.nix | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 56dfb71..38bc581 100644 --- a/flake.lock +++ b/flake.lock @@ -332,16 +332,17 @@ "nixtest": { "locked": { "dir": "lib", - "lastModified": 1748711200, - "narHash": "sha256-1Fx0jDk4ZsLkX3oSHo14OwzsK3sHkc2ykfz4bYqWuGA=", + "lastModified": 1748945995, + "narHash": "sha256-OWflapMq78nCUT7N0vS/AuK4E8y7p8oh0zjnDioJ4Lw=", "owner": "technofab", "repo": "nixtest", - "rev": "3ff5b358d506473ace2311cd25fb95be0c048997", + "rev": "392e8796f386373a36aecd3216925accf2714a1a", "type": "gitlab" }, "original": { "dir": "lib", "owner": "technofab", + "ref": "1.0.0", "repo": "nixtest", "type": "gitlab" } diff --git a/flake.nix b/flake.nix index e4e363f..0788dc0 100644 --- a/flake.nix +++ b/flake.nix @@ -15,10 +15,8 @@ systems = import systems; flake = {}; perSystem = { - lib, pkgs, config, - self', system, ... }: rec { @@ -287,7 +285,7 @@ treefmt-nix.url = "github:numtide/treefmt-nix"; nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami"; - nixtest.url = "gitlab:technofab/nixtest?dir=lib"; + nixtest.url = "gitlab:technofab/nixtest/1.0.0?dir=lib"; }; nixConfig = { From 625fe732b55d555d82d7c949aa66c2017f650c88 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Tue, 8 Jul 2025 09:30:02 +0200 Subject: [PATCH 061/102] feat: customizable CI stages --- templates/nix-gitlab-ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index cc87788..2dcead3 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -23,10 +23,18 @@ spec: type: string description: | Which version of the Nix CI image to use. Using a tag/version is recommended. + stage_build: + type: string + description: The CI stage for building the dynamic pipeline. + default: build + stage_trigger: + type: string + description: The CI stage for triggering the dynamic pipeline. + default: trigger --- stages: - - build - - trigger + - $[[ inputs.stage_build ]] + - $[[ inputs.stage_trigger ]] variables: # These can be overriden, see https://docs.gitlab.com/ci/variables/#cicd-variable-precedence # which image should be used by default. @@ -34,7 +42,7 @@ variables: # default cache stategy NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] nix-ci:build: - stage: build + stage: $[[ inputs.stage_build ]] image: $NIX_CI_IMAGE cache: - key: @@ -92,7 +100,7 @@ nix-ci:build: dotenv: trigger.env nix-ci:trigger: - stage: trigger + stage: $[[ inputs.stage_trigger ]] needs: - nix-ci:build trigger: From 0952ab414561fcadc0ffb5051f5c29456c0df789 Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 1 Sep 2025 15:04:20 +0200 Subject: [PATCH 062/102] feat: initial v3 rewrite --- .envrc | 2 + .gitignore | 2 + flake.lock | 63 ++++++++++++ flake.nix | 37 +++++++ flakeModuleTest/flake.lock | 77 ++++++++++++++ flakeModuleTest/flake.nix | 43 ++++++++ lib/VERSION | 1 + lib/default.nix | 26 +++++ lib/flake.nix | 6 ++ lib/flakeModule.nix | 24 +++++ lib/impl/default.nix | 12 +++ lib/impl/helpers.nix | 90 ++++++++++++++++ lib/impl/jobDeps.nix | 35 +++++++ lib/impl/jobPatched.nix | 43 ++++++++ lib/impl/jobRun.nix | 47 +++++++++ lib/impl/modules/default.nix | 32 ++++++ lib/impl/modules/job.nix | 131 ++++++++++++++++++++++++ lib/impl/modules/pipeline.nix | 101 ++++++++++++++++++ lib/impl/modules/root.nix | 61 +++++++++++ lib/impl/modules/soonix.nix | 61 +++++++++++ lib/impl/pipeline.nix | 61 +++++++++++ lib/impl/sandbox_helper.sh | 73 +++++++++++++ lib/utils.nix | 46 +++++++++ nix/packages/pkgs.nix | 48 +++++++++ nix/packages/scripts/finalize_nix_ci.sh | 40 ++++++++ nix/packages/scripts/setup_nix_ci.sh | 48 +++++++++ nix/repo/ci.nix | 45 ++++++++ nix/repo/devShells.nix | 27 +++++ nix/repo/docs.nix | 1 + nix/repo/flake.lock | 45 ++++++++ nix/repo/flake.nix | 16 +++ templates/nix-gitlab-ci.yml | 113 ++++++++++++++++++++ 32 files changed, 1457 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 flakeModuleTest/flake.lock create mode 100644 flakeModuleTest/flake.nix create mode 100644 lib/VERSION create mode 100644 lib/default.nix create mode 100644 lib/flake.nix create mode 100644 lib/flakeModule.nix create mode 100644 lib/impl/default.nix create mode 100644 lib/impl/helpers.nix create mode 100644 lib/impl/jobDeps.nix create mode 100644 lib/impl/jobPatched.nix create mode 100644 lib/impl/jobRun.nix create mode 100644 lib/impl/modules/default.nix create mode 100644 lib/impl/modules/job.nix create mode 100644 lib/impl/modules/pipeline.nix create mode 100644 lib/impl/modules/root.nix create mode 100644 lib/impl/modules/soonix.nix create mode 100644 lib/impl/pipeline.nix create mode 100644 lib/impl/sandbox_helper.sh create mode 100644 lib/utils.nix create mode 100644 nix/packages/pkgs.nix create mode 100644 nix/packages/scripts/finalize_nix_ci.sh create mode 100644 nix/packages/scripts/setup_nix_ci.sh create mode 100644 nix/repo/ci.nix create mode 100644 nix/repo/devShells.nix create mode 100644 nix/repo/docs.nix create mode 100644 nix/repo/flake.lock create mode 100644 nix/repo/flake.nix create mode 100644 templates/nix-gitlab-ci.yml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..565a52a --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source $(fetchurl https://gitlab.com/rensa-nix/direnv/-/raw/v0.3.0/direnvrc "sha256-u7+KEz684NnIZ+Vh5x5qLrt8rKdnUNexewBoeTcEVHQ=") +use ren //repo/devShells/default diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe07db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ren/ +result diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7c47f5c --- /dev/null +++ b/flake.lock @@ -0,0 +1,63 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1756542300, + "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754184128, + "narHash": "sha256-AjhoyBL4eSyXf01Bmc6DiuaMrJRNdWopmdnMY0Pa/M0=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "02e72200e6d56494f4a7c0da8118760736e41b60", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "ren": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "dir": "lib", + "lastModified": 1756370106, + "narHash": "sha256-l84ojcHuQWBwn4BRxQsMMfQpcq/Az/sHh/hSqFgVtyg=", + "owner": "rensa-nix", + "repo": "core", + "rev": "9c1a29fa9ba7cbbb78b9e47eb8afbcd29303a3b4", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "rensa-nix", + "repo": "core", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "ren": "ren" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ea47bb8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + ren.url = "gitlab:rensa-nix/core?dir=lib"; + }; + + outputs = { + self, + ren, + ... + } @ inputs: + ren.buildWith + { + inherit inputs; + cellsFrom = ./nix; + transformInputs = system: i: + i + // { + pkgs = import i.nixpkgs {inherit system;}; + }; + cellBlocks = with ren.blocks; [ + (simple "devShells") + (simple "pkgs") + # (simple "tests") + # (simple "docs") + (simple "ci") + ]; + } + { + packages = ren.select self [ + # ["repo" "tests"] + # ["repo" "docs"] + ["repo" "ci" "packages"] + ["packages" "pkgs"] + ]; + }; +} diff --git a/flakeModuleTest/flake.lock b/flakeModuleTest/flake.lock new file mode 100644 index 0000000..15b9835 --- /dev/null +++ b/flakeModuleTest/flake.lock @@ -0,0 +1,77 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1756636162, + "narHash": "sha256-mBecwgUTWRgClJYqcF+y4O1bY8PQHqeDpB+zsAn+/zA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "37ff64b7108517f8b6ba5705ee5085eac636a249", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flakeModuleTest/flake.nix b/flakeModuleTest/flake.nix new file mode 100644 index 0000000..92d5c87 --- /dev/null +++ b/flakeModuleTest/flake.nix @@ -0,0 +1,43 @@ +{ + outputs = { + flake-parts, + systems, + ... + } @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + ../lib/flakeModule.nix + ]; + systems = import systems; + flake = {}; + perSystem = { + ... + }: { + ci = { + config = { + # true is already default, just for testing + nixJobsByDefault = true; + }; + pipelines = { + "default" = { + stages = ["example"]; + jobs."example" = { + stage = "example"; + script = ["echo hello world"]; + }; + }; + "test".jobs."example" = { + stage = ".pre"; + script = ["echo hello world"]; + }; + }; + }; + }; + }; + + 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/lib/VERSION b/lib/VERSION new file mode 100644 index 0000000..0786c6b --- /dev/null +++ b/lib/VERSION @@ -0,0 +1 @@ +3.0.0-alpha.1 diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..d18e747 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,26 @@ +args: let + # allow passing just pkgs aswell for convenience + lib = args.lib or args.pkgs.lib; + # makes it optional to pass if it's not explicitly needed + pkgs = args.pkgs or (throw "[nix-gitlab-ci] pkgs argument was used but not set, please pass it"); + inherit (lib) evalModules; + + impl = import ./impl {inherit lib pkgs cilib;}; + + cilib = { + inherit (impl) helpers modules mkPipeline mkJobRun mkJobDeps mkJobPatched; + utils = import ./utils.nix {inherit pkgs;}; + version = builtins.readFile ./VERSION; + + mkCI = config: + (evalModules { + modules = [ + cilib.modules.nixCiSubmodule + { + inherit config; + } + ]; + }).config; + }; +in + cilib diff --git a/lib/flake.nix b/lib/flake.nix new file mode 100644 index 0000000..227f9a6 --- /dev/null +++ b/lib/flake.nix @@ -0,0 +1,6 @@ +{ + outputs = _i: { + lib = import ./.; + flakeModule = ./flakeModule.nix; + }; +} diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix new file mode 100644 index 0000000..aadf292 --- /dev/null +++ b/lib/flakeModule.nix @@ -0,0 +1,24 @@ +{ + flake-parts-lib, + lib, + ... +}: { + options.perSystem = flake-parts-lib.mkPerSystemOption ( + { + config, + pkgs, + ... + }: let + cilib = import ./. {inherit lib pkgs;}; + inherit (lib) types mkOption; + in { + options = { + ci = mkOption { + type = types.submodule cilib.modules.nixCiSubmodule; + default = {}; + }; + }; + config.legacyPackages = config.ci.packages; + } + ); +} diff --git a/lib/impl/default.nix b/lib/impl/default.nix new file mode 100644 index 0000000..7ff5623 --- /dev/null +++ b/lib/impl/default.nix @@ -0,0 +1,12 @@ +{ + pkgs, + lib, + cilib, +}: rec { + helpers = import ./helpers.nix {inherit lib pkgs;}; + mkJobDeps = import ./jobDeps.nix {inherit lib helpers;}; + 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;}; +} diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix new file mode 100644 index 0000000..38dc030 --- /dev/null +++ b/lib/impl/helpers.nix @@ -0,0 +1,90 @@ +{ + pkgs, + lib, +} @ args: let + inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType; +in rec { + prepend = key: arr: job: { + ${key} = arr ++ (job.${key} or []); + }; + append = key: arr: job: { + ${key} = (job.${key} or []) ++ arr; + }; + prependToBeforeScript = prepend "before_script"; + appendToAfterScript = append "after_script"; + + # json is also valid yaml and this removes dependency on jq and/or remarshal + # (used in pkgs.formats.json and pkgs.formats.yaml respectively) + toYaml = name: value: builtins.toFile name (builtins.toJSON value); + toYamlPretty = (pkgs.formats.yaml {}).generate; + + customMapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); + + filterAttrsRec = pred: v: + if isAttrs v + then filterAttrs pred (mapAttrs (_path: filterAttrsRec pred) v) + else v; + + # 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 {}); + + deepMerge = lhs: rhs: + lhs + // rhs + // (builtins.mapAttrs ( + rName: rValue: let + lValue = lhs.${rName} or null; + in + if builtins.isAttrs lValue && builtins.isAttrs rValue + then deepMerge lValue rValue + else if builtins.isList lValue && builtins.isList rValue + then lValue ++ rValue + else rValue + ) + rhs); + + unsetType = mkOptionType { + name = "unset"; + description = "unset"; + descriptionClass = "noun"; + check = _value: true; + }; + unset = { + _type = "unset"; + }; + isUnset = isType "unset"; + unsetOr = types.either unsetType; + mkUnsetOption = opts: + mkOption (opts + // { + type = unsetOr opts.type; + default = opts.default or unset; + }); + + filterUnset = value: + if builtins.isAttrs value && !builtins.hasAttr "_type" value + then let + filteredAttrs = builtins.mapAttrs (_n: filterUnset) value; + in + filterAttrs (_name: value: (!isUnset value)) filteredAttrs + else if builtins.isList value + then builtins.filter (elem: !isUnset elem) (map filterUnset value) + else value; + + # args.pkgs so "pkgs" does not need to be passed all the time + stdenvMinimal = args.pkgs.stdenvNoCC.override { + cc = null; + preHook = ""; + allowedRequisites = null; + initialPath = with args.pkgs; [coreutils findutils]; + extraNativeBuildInputs = []; + }; +} diff --git a/lib/impl/jobDeps.nix b/lib/impl/jobDeps.nix new file mode 100644 index 0000000..302597d --- /dev/null +++ b/lib/impl/jobDeps.nix @@ -0,0 +1,35 @@ +{ + lib, + helpers, +}: let + inherit (lib) concatLines mapAttrsToList makeBinPath; + inherit (helpers) filterJobVariables stdenvMinimal; +in + { + key, + job, + nixConfig, + }: let + variablesWithStorePaths = filterJobVariables true job; + variableExports = concatLines ( + mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths + ); + script = '' + export PATH="${makeBinPath (nixConfig.deps or [])}:$PATH"; + # variables containing nix derivations: + ${variableExports} + ''; + in + stdenvMinimal.mkDerivation { + name = "gitlab-ci-job-deps-${key}"; + dontUnpack = true; + installPhase = + # sh + '' + echo '${script}' > $out + chmod +x $out + ''; + passthru = { + inherit script; + }; + } diff --git a/lib/impl/jobPatched.nix b/lib/impl/jobPatched.nix new file mode 100644 index 0000000..4bf0e00 --- /dev/null +++ b/lib/impl/jobPatched.nix @@ -0,0 +1,43 @@ +{ + lib, + helpers, +}: let + inherit (lib) toList optionalAttrs optional; + inherit (helpers) prependToBeforeScript appendToAfterScript filterJobVariables; +in + { + key, + job, + pipelineName, + nixConfig, + }: + job + // (optionalAttrs nixConfig.enable ( + (prependToBeforeScript ["source setup_nix_ci \"gitlab-ci:pipeline:${pipelineName}:job-deps:${key}\""] job) + // (appendToAfterScript ["finalize_nix_ci"] job) + )) + // optionalAttrs nixConfig.enable ( + (let + variables = + (filterJobVariables false job) + // optionalAttrs nixConfig.enableRunnerCache { + NIX_CI_CACHE_STRATEGY = "runner"; + }; + in + # filter empty variables + optionalAttrs (variables != {}) { + inherit variables; + }) + // (let + cache = + (toList (job.cache or [])) + ++ (optional nixConfig.enableRunnerCache { + key = nixConfig.runnerCacheKey; + paths = [".nix-cache/"]; + }); + in + # filter empty cache + optionalAttrs (cache != []) { + inherit cache; + }) + ) diff --git a/lib/impl/jobRun.nix b/lib/impl/jobRun.nix new file mode 100644 index 0000000..4a1e0f6 --- /dev/null +++ b/lib/impl/jobRun.nix @@ -0,0 +1,47 @@ +{ + lib, + pkgs, + helpers, +}: let + inherit (lib) concatLines mapAttrsToList getExe; + inherit (helpers) filterJobVariables; +in + { + key, + job, + jobDeps, + }: let + variablesWithoutStorePaths = filterJobVariables false job; + variableExports = concatLines ( + mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithoutStorePaths + ); + sandboxHelper = pkgs.writeShellScriptBin "gitlab-ci-job-sandbox-helper" (builtins.readFile ./sandbox_helper.sh); + actualJobScript = pkgs.writeShellScript "gitlab-ci-job:${key}:raw" '' + # set up deps and environment variables containing store paths + . ${jobDeps} + # normal environment variables + ${variableExports} + # run before_script, script and after_script + echo -e "\e[32mRunning before_script...\e[0m" + set -x + ${concatLines (job.before_script or [])} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning script...\e[0m" + set -x + ${concatLines job.script} + { set +x; } 2>/dev/null + echo -e "\e[32mRunning after_script...\e[0m" + set -x + ${concatLines (job.after_script or [])} + { set +x; } 2>/dev/null + ''; + in + # this way the sandbox helper just needs to be built once + pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' + exec ${getExe sandboxHelper} ${actualJobScript} $@ + '' + // { + passthru = { + inherit jobDeps actualJobScript; + }; + } diff --git a/lib/impl/modules/default.nix b/lib/impl/modules/default.nix new file mode 100644 index 0000000..9e33a7e --- /dev/null +++ b/lib/impl/modules/default.nix @@ -0,0 +1,32 @@ +{ + lib, + cilib, +}: rec { + inherit + (import ./root.nix { + inherit lib pipelineSubmodule soonixSubmodule; + }) + configSubmodule + nixCiSubmodule + ; + inherit + (import ./pipeline.nix { + inherit lib cilib jobSubmodule; + }) + pipelineConfigSubmodule + pipelineSubmodule + ; + inherit + (import ./job.nix { + inherit lib cilib; + }) + jobConfigSubmodule + jobSubmodule + ; + inherit + (import ./soonix.nix { + inherit lib cilib; + }) + soonixSubmodule + ; +} diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix new file mode 100644 index 0000000..50340f6 --- /dev/null +++ b/lib/impl/modules/job.nix @@ -0,0 +1,131 @@ +{ + lib, + cilib, + ... +}: let + inherit (lib) mkOption types filterAttrs; + inherit (cilib.helpers) filterUnset mkUnsetOption; +in rec { + jobConfigSubmodule = {pipelineConfig, ...}: { + options = { + enable = mkOption { + description = "Transform this job to a nix-configured one"; + type = types.bool; + default = pipelineConfig.nixJobsByDefault; + }; + deps = mkOption { + description = "Dependencies to inject into the job before running it"; + type = types.listOf types.package; + default = []; + }; + enableRunnerCache = mkOption { + type = types.bool; + default = false; + description = '' + Cache this job using the GitLab Runner cache. + Warning: useful for tiny jobs, but most of the time it just takes an eternity. + ''; + }; + runnerCacheKey = mkOption { + type = types.str; + default = "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"; + description = "Cache key to use for the runner nix cache. Requires enableRunnerCache = true"; + }; + }; + }; + + jobSubmodule = { + name, + config, + pipelineName, + pipelineConfig, + ... + }: let + # + # GITLAB OPTIONS + # + gitlabOptions = { + stage = mkOption { + type = types.str; + }; + image = mkUnsetOption { + type = types.str; + }; + variables = mkUnsetOption { + type = types.attrsOf types.str; + }; + before_script = mkUnsetOption { + type = types.listOf types.str; + }; + script = mkOption { + type = types.listOf types.str; + }; + after_script = mkUnsetOption { + type = types.listOf types.str; + }; + artifacts = mkUnsetOption { + type = types.attrs; # TODO: more granular + description = ''''; + }; + rules = mkUnsetOption { + type = types.listOf types.attrs; + }; + allow_failure = mkUnsetOption { + type = types.bool; + }; + }; + in { + options = + { + nix = mkOption { + description = "Nix-GitLab-CI config options for this job"; + type = types.submoduleWith { + modules = [jobConfigSubmodule]; + specialArgs.pipelineConfig = pipelineConfig; + }; + default = {}; + }; + finalConfig = mkOption { + internal = true; + type = types.attrs; + }; + depsDrv = mkOption { + internal = true; + type = types.package; + }; + runnerDrv = mkOption { + internal = true; + type = types.package; + }; + packages = mkOption { + internal = true; + type = types.attrsOf types.package; + }; + } + // gitlabOptions; + config = let + attrsToKeep = builtins.attrNames gitlabOptions; + in { + finalConfig = cilib.mkJobPatched { + key = name; + job = filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config); + nixConfig = config.nix; + inherit pipelineName; + }; + depsDrv = cilib.mkJobDeps { + key = name; + job = config.finalConfig; + nixConfig = config.nix; + }; + runnerDrv = cilib.mkJobRun { + key = name; + job = config.finalConfig; + jobDeps = config.depsDrv; + }; + packages = { + "gitlab-ci:pipeline:${pipelineName}:job-deps:${name}" = config.depsDrv; + "gitlab-ci:pipeline:${pipelineName}:job:${name}" = config.runnerDrv; + }; + }; + }; +} diff --git a/lib/impl/modules/pipeline.nix b/lib/impl/modules/pipeline.nix new file mode 100644 index 0000000..e439b4d --- /dev/null +++ b/lib/impl/modules/pipeline.nix @@ -0,0 +1,101 @@ +{ + lib, + cilib, + jobSubmodule, + ... +}: let + inherit (lib) mkOption types filterAttrs mergeAttrsList pipe mapAttrs; + inherit (cilib.helpers) filterUnset mkUnsetOption toYaml toYamlPretty; + + pipelineConfigSubmodule = {rootConfig, ...}: { + options = { + nixJobsByDefault = mkOption { + description = '' + Whether to transform all jobs to nix-configured jobs by default. + If false, you need to set `nix.enable` for each job you want to be transformed. + ''; + type = types.bool; + default = rootConfig.nixJobsByDefault; + }; + }; + }; + + pipelineSubmodule = { + name, + config, + rootConfig, + ... + }: let + # + # GITLAB OPTIONS + # + gitlabOptions = { + stages = mkOption { + type = types.listOf types.str; + default = []; + # .pre and .post always exist + apply = val: [".pre"] ++ val ++ [".post"]; + }; + variables = mkUnsetOption { + type = types.attrsOf types.str; + description = ''''; + }; + }; + in { + _file = ./pipeline.nix; + options = + { + nix = mkOption { + description = "Nix-CI config options for this pipeline"; + type = types.submoduleWith { + modules = [pipelineConfigSubmodule]; + specialArgs.rootConfig = rootConfig; + }; + default = {}; + }; + finalConfig = mkOption { + description = "Final config of the pipeline"; + internal = true; + type = types.attrs; + }; + packages = mkOption { + description = "Final packages for use in CI"; + internal = true; + type = types.attrsOf types.package; + }; + # jobs are nested to make distinguishing them from other keys in the ci config easier + jobs = mkOption { + description = "Jobs for this pipeline"; + type = types.attrsOf (types.submoduleWith { + modules = [jobSubmodule]; + specialArgs = { + pipelineName = name; + pipelineConfig = config.nix; + }; + }); + default = {}; + }; + } + // gitlabOptions; + config = let + attrsToKeep = builtins.attrNames gitlabOptions; + in { + finalConfig = + (filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config)) + // mapAttrs (_name: value: value.finalConfig) config.jobs; + packages = + { + "gitlab-ci:pipeline:${name}" = pipe config.finalConfig [ + builtins.toJSON + builtins.unsafeDiscardOutputDependency + builtins.unsafeDiscardStringContext + (toYaml "gitlab-ci-config.json") + ]; + "gitlab-ci:pipeline:${name}:pretty" = toYamlPretty "gitlab-ci-config.yml" config.finalConfig; + } + // mergeAttrsList (map (job: job.packages) (builtins.attrValues config.jobs)); + }; + }; +in { + inherit pipelineSubmodule pipelineConfigSubmodule; +} diff --git a/lib/impl/modules/root.nix b/lib/impl/modules/root.nix new file mode 100644 index 0000000..44ef866 --- /dev/null +++ b/lib/impl/modules/root.nix @@ -0,0 +1,61 @@ +{ + lib, + soonixSubmodule, + pipelineSubmodule, + ... +}: let + inherit (lib) mkOption types; +in rec { + configSubmodule = { + options = { + soonix = mkOption { + description = "Configure the soonix '.gitlab-ci.yml' generation"; + type = types.submodule soonixSubmodule; + default = {}; + }; + nixJobsByDefault = mkOption { + description = '' + Whether to transform all jobs to nix-configured jobs by default. + If false, you need to set `nix.enable` for each job you want to be transformed. + ''; + type = types.bool; + default = true; + }; + }; + }; + + nixCiSubmodule = {config, ...}: { + options = { + config = mkOption { + description = "Configuration of Nix-GitLab-CI itself"; + type = types.submodule configSubmodule; + default = {}; + }; + pipelines = mkOption { + description = "Defines all pipelines"; + type = types.attrsOf (types.submoduleWith { + modules = [pipelineSubmodule]; + specialArgs.rootConfig = config.config; + }); + default = {}; + }; + + packages = mkOption { + description = "Final packages for use in CI"; + internal = true; + type = types.attrsOf types.package; + }; + soonix = mkOption { + description = "Soonix config for .gitlab-ci.yml"; + internal = true; + type = types.attrs; + }; + }; + config = { + packages = lib.fold (pipeline: acc: acc // pipeline) {} ( + map (pipeline: pipeline.packages) (builtins.attrValues config.pipelines) + ); + soonix = config.config.soonix.finalConfig; + }; + }; +} diff --git a/lib/impl/modules/soonix.nix b/lib/impl/modules/soonix.nix new file mode 100644 index 0000000..6c7fd21 --- /dev/null +++ b/lib/impl/modules/soonix.nix @@ -0,0 +1,61 @@ +{ + lib, + cilib, + ... +}: let + inherit (lib) mkOption types; +in { + soonixSubmodule = {config, ...}: { + options = { + componentVersion = mkOption { + description = "CI/CD component version. Also get's passed to inputs → version"; + type = types.str; + default = cilib.version; + }; + componentUrl = mkOption { + description = "CI/CD component url"; + type = types.str; + default = "gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci"; + }; + componentInputs = mkOption { + description = "Extra inputs to pass to the CI/CD component"; + type = types.attrs; + default = {}; + }; + extraData = mkOption { + description = "Extra data to include in the .gitlab-ci.yml file"; + type = types.attrs; + default = {}; + }; + + finalConfig = mkOption { + internal = true; + type = types.attrs; + }; + }; + config.finalConfig = { + opts.format = "yaml"; + hook = { + mode = "copy"; + gitignore = false; + }; + output = ".gitlab-ci.yml"; + generator = "nix"; + data = + cilib.helpers.deepMerge + { + include = [ + { + component = "${config.componentUrl}@${config.componentVersion}"; + inputs = + { + version = config.componentVersion; + } + // config.componentInputs; + } + ]; + } + config.extraData; + }; + }; +} diff --git a/lib/impl/pipeline.nix b/lib/impl/pipeline.nix new file mode 100644 index 0000000..4d9b185 --- /dev/null +++ b/lib/impl/pipeline.nix @@ -0,0 +1,61 @@ +{ + lib, + helpers, + mkJobDeps, + mkJobRun, + mkJobPatched, +}: let + inherit (lib) assertMsg; + inherit (helpers) filterAttrsRec customMapAttrs toYaml toYamlPretty; +in + { + name, + pipeline, + nixConfig, + }: let + jobs = filterAttrsRec (_n: v: v != null) pipeline.jobs; + rest = filterAttrsRec (_n: v: v != null) (builtins.removeAttrs pipeline ["jobs"]); + # 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 = + customMapAttrs (key: job: { + name = "gitlab-ci:pipeline:${name}:job-deps:${key}"; + value = mkJobDeps {inherit key job nixConfig;}; + }) + jobs; + # allows the user to directly run the script + jobsMappedForScript = + customMapAttrs (key: job: { + name = "gitlab-ci:pipeline:${name}:job:${key}"; + value = mkJobRun { + inherit key job nixConfig; + jobDeps = jobsMappedForDeps."gitlab-ci:pipeline:${name}:job-deps:${key}"; + }; + }) + jobs; + # build the deps specific for this job before anything, this way the deps should be fetched from the cache + jobsPatched = + customMapAttrs (key: job: { + name = key; + value = assert assertMsg (builtins.elem job.stage (rest.stages or [])) "stage '${job.stage}' of job '${key}' does not exist"; + mkJobPatched { + inherit key job nixConfig; + pipelineName = name; + }; + }) + jobs; + in { + packages = + # gitlab-ci:pipeline: + # gitlab-ci:pipeline::job: + # gitlab-ci:pipeline::job-deps: + { + "gitlab-ci:pipeline:${name}" = toYaml "gitlab-ci-${name}.yml" (rest // jobsPatched); + "gitlab-ci:pipeline:${name}:pretty" = toYamlPretty "gitlab-ci-${name}.yml" (rest // jobsPatched); + } + // jobsMappedForDeps + // jobsMappedForScript; + finalConfig = rest // jobsPatched; + } diff --git a/lib/impl/sandbox_helper.sh b/lib/impl/sandbox_helper.sh new file mode 100644 index 0000000..4ea5ec5 --- /dev/null +++ b/lib/impl/sandbox_helper.sh @@ -0,0 +1,73 @@ +echo -e "\e[32mSetting up...\e[0m" + +actualJobScript=$1 +shift + +INCLUDE_DIRTY=false +NO_SANDBOX=false +KEEP_TMP=false +KEEP_ENV="" +# parse flags +while [[ $# -gt 0 ]]; do + case "$1" in + --include-dirty) + INCLUDE_DIRTY=true + shift + ;; + --no-sandbox) + NO_SANDBOX=true + shift + ;; + --keep-tmp) + KEEP_TMP=true + shift + ;; + --keep-env) + KEEP_ENV="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [ "$NO_SANDBOX" = false ]; then + echo "Running with simple sandboxing" + if [ "$KEEP_TMP" = false ]; then + trap "rm -rf '$TMPDIR'" EXIT + else + echo "Temp dir will be preserved at: $TMPDIR" + fi + + # check if dirty + DIRTY_PATCH="" + if ! git diff --quiet && ! git diff --staged --quiet; then + echo "Warning: working tree is dirty." + DIRTY_PATCH=$(mktemp -t "nix-gitlab-ci.XXX.patch") + git diff --staged > "$DIRTY_PATCH" + trap "rm -f '$DIRTY_PATCH'" EXIT + fi + TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") + git clone . $TMPDIR + pushd $TMPDIR >/dev/null + if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then + echo "Copying dirty changes..." + git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" + fi + + echo "Running job in $TMPDIR" + env -i $( + if [[ -n "$KEEP_ENV" ]]; then + IFS=',' read -ra VARS <<< "$KEEP_ENV" + for var in "${VARS[@]}"; do + printf '%s=%q ' "$var" "${!var}" + done + fi + ) bash $actualJobScript + popd >/dev/null +else + exec $actualJobScript +fi + diff --git a/lib/utils.nix b/lib/utils.nix new file mode 100644 index 0000000..ae15284 --- /dev/null +++ b/lib/utils.nix @@ -0,0 +1,46 @@ +{pkgs, ...}: { + commitAndPushFiles = { + message, + files ? [], + }: jobArgs: + jobArgs + // { + before_script = + (jobArgs.before_script or []) + ++ [ + # sh + '' + echo -e "\\e[0Ksection_start:`date +%s`:commit_setup[collapsed=true]\\r\\e[0KSetting up commitAndPushFiles" + eval "$(ssh-agent -s)" >/dev/null; + mkdir -p ~/.ssh; touch ~/.ssh/known_hosts; + ssh-keyscan -t rsa $CI_SERVER_HOST >> ~/.ssh/known_hosts; + echo "$GIT_SSH_PRIV_KEY" | tr -d '\r' | ssh-add - >/dev/null; + git config --global user.email "$GIT_EMAIL" >/dev/null; + git config --global user.name "$GIT_NAME" >/dev/null; + export CI_PUSH_REPO=`echo $CI_REPOSITORY_URL | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:|"`; + git remote rm origin && git remote add origin ''${CI_PUSH_REPO} + echo -e "\\e[0Ksection_end:`date +%s`:commit_setup\\r\\e[0K" + '' + ]; + script = let + addScript = + if builtins.length files == 0 + then "" + else "git add ${builtins.concatStringsSep " " files}"; + in + (jobArgs.script or []) + ++ [ + # sh + '' + echo -e "\\e[0Ksection_start:`date +%s`:commit[collapsed=true]\\r\\e[0KCommiting & pushing changes if necessary" + ${addScript} + git diff --cached --exit-code >/dev/null && + echo "Nothing to commit" || + git commit -m "${message}" --no-verify; + git push --tags origin ''${GIT_SOURCE_REF:-HEAD}:''${GIT_TARGET_REF:-$CI_COMMIT_REF_NAME} -o ci.skip + echo -e "\\e[0Ksection_end:`date +%s`:commit\\r\\e[0K" + '' + ]; + nix.deps = (jobArgs.nix.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; + }; +} diff --git a/nix/packages/pkgs.nix b/nix/packages/pkgs.nix new file mode 100644 index 0000000..34d07aa --- /dev/null +++ b/nix/packages/pkgs.nix @@ -0,0 +1,48 @@ +{ + inputs, + system, + ... +}: let + inherit (inputs) pkgs; +in rec { + setupScript = pkgs.writeShellScriptBin "setup_nix_ci" (builtins.readFile ./scripts/setup_nix_ci.sh); + finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" (builtins.readFile ./scripts/finalize_nix_ci.sh); + image = pkgs.dockerTools.buildImage { + name = "nix-ci"; + fromImage = let + hashes = { + "x86_64-linux" = "sha256-kJ7dqje5o1KPr3RDZ7/THbhMSoiCU1C/7HshDrNfwnM="; + "aarch64-linux" = "sha256-jz+Z3Ji+hy5d9ImOh/YOKCqy9P9/cseSov+5J/O95bg="; + }; + # check digest of tags like nixos-24.11-aarch64-linux etc. + digests = { + "x86_64-linux" = "sha256:345f210dea4cbd049e2d01d13159c829066dfb6e273cdd49ea878186d17b19f7"; + "aarch64-linux" = "sha256:66163fdf446d851416dd4e9be28c0794d9c2550214a57a846957699a3f5747f6"; + }; + hash = hashes.${system} or (throw "Unsupported system"); + imageDigest = digests.${system} or (throw "Unsupported system"); + in + pkgs.dockerTools.pullImage { + imageName = "nixpkgs/nix-flakes"; + inherit hash imageDigest; + }; + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = with pkgs; + [ + gitMinimal + gnugrep + gnused + coreutils + diffutils + cachix + attic-client + ] + ++ [ + setupScript + finalizeScript + ]; + pathsToLink = ["/bin"]; + }; + }; +} diff --git a/nix/packages/scripts/finalize_nix_ci.sh b/nix/packages/scripts/finalize_nix_ci.sh new file mode 100644 index 0000000..ecc2211 --- /dev/null +++ b/nix/packages/scripts/finalize_nix_ci.sh @@ -0,0 +1,40 @@ +echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." + nix path-info --all > /tmp/nix-store-after + echo "Finding new paths..." + NEW_PATHS=$(diff --new-line-format="%L" \ + --old-line-format="" --unchanged-line-format="" \ + /tmp/nix-store-before /tmp/nix-store-after) + COUNT=$(wc -l <<<"$NEW_PATHS") + + if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then + export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; + fi + + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" + echo -n "$NEW_PATHS" | { + case "$NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + # add ^* to all store paths ending in .drv (prevent warning log spam) + sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true + ;; + "attic") + attic push --stdin ci:$ATTIC_CACHE || true + ;; + "cachix") + cachix push $CACHIX_CACHE || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" + ;; + esac + } + echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" + else + echo "Caching disabled, not uploading $COUNT new store entries..." + fi +echo -e "\\e[0Ksection_end:`date +%s`:finalize_nix_ci\\r\\e[0K" diff --git a/nix/packages/scripts/setup_nix_ci.sh b/nix/packages/scripts/setup_nix_ci.sh new file mode 100644 index 0000000..eaf189b --- /dev/null +++ b/nix/packages/scripts/setup_nix_ci.sh @@ -0,0 +1,48 @@ +echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" + nix path-info --all > /tmp/nix-store-before + + if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then + export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; + echo "NIX_CI_CACHE_STRATEGY was set to auto, selected '$NIX_CI_CACHE_STRATEGY' for this job" + fi + + if [ -z "$NIX_CI_DISABLE_CACHE" ]; then + echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" + case "$NIX_CI_CACHE_STRATEGY" in + "runner") + export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} + echo "Runner Cache: $RUNNER_CACHE" + export NIX_CONFIG="$NIX_CONFIG + extra-trusted-substituters = $RUNNER_CACHE?priority=10&trusted=true + extra-substituters = $RUNNER_CACHE?priority=10&trusted=true + " + ;; + "attic") + echo "Attic Cache: $ATTIC_CACHE" + attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true + attic use "$ATTIC_CACHE" || true + ;; + "cachix") + echo "Cachix Cache: $CACHIX_CACHE" + cachix use "$CACHIX_CACHE" || true + ;; + "none") + echo "Cache strategy is none, doing nothing..." + ;; + *) + echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" + ;; + esac + echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" + else + echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration..." + fi + + # load the job's deps only if the name was passed + if [[ ! -z $1 ]]; then + echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching Nix dependencies for job" + nix build .#$1 + source $(readlink -f result) + echo -e "\\e[0Ksection_end:`date +%s`:nix_deps\\r\\e[0K" + fi +echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix new file mode 100644 index 0000000..913d8fc --- /dev/null +++ b/nix/repo/ci.nix @@ -0,0 +1,45 @@ +{inputs, ...}: let + inherit (inputs) cilib; +in + cilib.mkCI { + pipelines."default" = { + stages = ["test" "build" "deploy"]; + jobs = { + "test" = { + stage = "test"; + script = [ + "nix run .#tests -- --junit=junit.xml" + ]; + allow_failure = true; + artifacts = { + when = "always"; + reports.junit = "junit.xml"; + }; + }; + "docs" = { + stage = "build"; + script = [ + # sh + '' + nix build .#docs:default + mkdir -p public + cp -r result/. public/ + '' + ]; + artifacts.paths = ["public"]; + }; + "pages" = { + nix.enable = false; + image = "alpine:latest"; + stage = "deploy"; + script = ["true"]; + artifacts.paths = ["public"]; + rules = [ + { + "if" = "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"; + } + ]; + }; + }; + }; + } diff --git a/nix/repo/devShells.nix b/nix/repo/devShells.nix new file mode 100644 index 0000000..c754595 --- /dev/null +++ b/nix/repo/devShells.nix @@ -0,0 +1,27 @@ +{inputs, ...}: let + inherit (inputs) pkgs devshell treefmt; +in { + default = devshell.mkShell { + packages = [ + pkgs.nil + (treefmt.mkWrapper pkgs { + programs = { + alejandra.enable = true; + deadnix.enable = true; + statix.enable = true; + mdformat.enable = true; + yamlfmt.enable = true; + }; + settings.formatter = { + yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; + mdformat.command = let + pkg = pkgs.python3.withPackages (p: [ + p.mdformat + p.mdformat-mkdocs + ]); + in "${pkg}/bin/mdformat"; + }; + }) + ]; + }; +} diff --git a/nix/repo/docs.nix b/nix/repo/docs.nix new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/nix/repo/docs.nix @@ -0,0 +1 @@ +{} diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock new file mode 100644 index 0000000..6b16df1 --- /dev/null +++ b/nix/repo/flake.lock @@ -0,0 +1,45 @@ +{ + "nodes": { + "devshell-lib": { + "locked": { + "dir": "lib", + "lastModified": 1755673398, + "narHash": "sha256-51MmR+Eo1+bKDd/Ss77wwTqi4yAR2xgmyCSEbKWSpj0=", + "owner": "rensa-nix", + "repo": "devshell", + "rev": "e76bef387e8a4574f9b6d37b1a424e706491af08", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "rensa-nix", + "repo": "devshell", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "devshell-lib": "devshell-lib", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "flake": false, + "locked": { + "lastModified": 1756662192, + "narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/repo/flake.nix b/nix/repo/flake.nix new file mode 100644 index 0000000..7d5e657 --- /dev/null +++ b/nix/repo/flake.nix @@ -0,0 +1,16 @@ +{ + inputs = { + devshell-lib.url = "gitlab:rensa-nix/devshell?dir=lib"; + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + flake = false; + }; + }; + outputs = i: + i + // { + devshell = i.devshell-lib.lib {inherit (i.parent) pkgs;}; + cilib = import "${i.parent.self}/lib" {inherit (i.parent) pkgs;}; + treefmt = import i.treefmt-nix; + }; +} diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml new file mode 100644 index 0000000..14eeaa9 --- /dev/null +++ b/templates/nix-gitlab-ci.yml @@ -0,0 +1,113 @@ +spec: + inputs: + cache_strategy: + type: string + description: | + auto (default) | none | runner | cachix | attic + Sets the default caching strategy. + - "auto": dynamically selects the best strategy for every job based on env variables + - "none": disables caching + - "runner", "cachix" & "attic": forces every job to use this strategy + + Can be overridden by setting NIX_CI_CACHE_STRATEGY in the pipeline variables. + default: "auto" + cache_files: + type: array + description: | + Files to use as the cache key for the generated pipeline yaml. + If you use "ci.nix" to define CI, add that here for example. + Note that max 2 items are allowed in cache:key:files, so use something like + ["flake.*", "ci.nix"] f. ex. to match flake.lock, flake.nix and ci.nix. + default: ["flake.nix", "flake.lock"] + version: + type: string + description: | + Which version of the Nix CI image to use. Using a tag/version is recommended. + stage_build: + type: string + description: The CI stage for building the dynamic pipeline. + default: build + stage_trigger: + type: string + description: The CI stage for triggering the dynamic pipeline. + default: trigger +--- +stages: + - $[[ inputs.stage_build ]] + - $[[ inputs.stage_trigger ]] +variables: + # These can be overriden, see https://docs.gitlab.com/ci/variables/#cicd-variable-precedence + # which image should be used by default. + NIX_CI_IMAGE: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$[[ inputs.version ]] + # default cache stategy + NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] +nix-ci:build: + stage: $[[ inputs.stage_build ]] + image: $NIX_CI_IMAGE + cache: + - key: + files: $[[ inputs.cache_files ]] + paths: + - .nix-ci-pipelines/ + - key: nix + paths: + - .nix-cache/ + before_script: + - | + # if no explicit pipeline is requested + if [[ -z "${NIX_CI_PIPELINE_NAME:-}" ]]; then + # if regex matches, use pipeline "default", otherwise $CI_PIPELINE_SOURCE + [[ "${CI_PIPELINE_SOURCE}" =~ ${NIX_CI_DEFAULT_SOURCES:-.*} ]] \ + && NIX_CI_PIPELINE_NAME="default" \ + || NIX_CI_PIPELINE_NAME="$CI_PIPELINE_SOURCE"; + fi + echo "NIX_CI_GENERATED_PIPELINE_NAME=$NIX_CI_PIPELINE_NAME" >> trigger.env + # inheritance of pipeline variables is a bit weird, so explicitly override them + # (ctx: setting any of these in the project variables would only apply correctly + # in this pipeline, not the child pipeline, instead weirdly enough the default + # variables above are used). If any other variables are added at the top, add them + # here aswell + echo "NIX_CI_IMAGE=$NIX_CI_IMAGE" >> trigger.env + echo "NIX_CI_CACHE_STRATEGY=$NIX_CI_CACHE_STRATEGY" >> trigger.env + + mkdir -p .nix-ci-pipelines/ + # generated-gitlab-ci.yml exists in the cache + [[ -f ".nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml" ]] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true + # allow the user to manually skip the cache (when the key files are not correctly configured etc.) + [[ -n "$NIX_CI_FORCE_BUILD" ]] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true + + # only setup when we need to generate the pipeline yaml + if [[ -z "$CACHED" ]]; then + source setup_nix_ci; + fi + script: + # build the pipeline if it does not exist in the cache + - > + if [[ -z "$CACHED" ]]; then + nix build .#gitlab-ci:pipeline:${NIX_CI_PIPELINE_NAME} && install result .nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml; + fi + after_script: + # save to binary cache or Gitlab CI cache only if we actually built something + # check if /tmp/nix-store-before exists as $CACHED never exists here and the file only exists if "setup_nix_ci" is called + - | + if [[ -f "/tmp/nix-store-before" ]]; then + finalize_nix_ci; + fi + artifacts: + paths: + - .nix-ci-pipelines/ + reports: + dotenv: trigger.env + +nix-ci:trigger: + stage: $[[ inputs.stage_trigger ]] + needs: + - nix-ci:build + trigger: + include: + - artifact: .nix-ci-pipelines/${NIX_CI_GENERATED_PIPELINE_NAME}.yml + job: nix-ci:build + strategy: depend + forward: + pipeline_variables: true + From e074d716c43189d440c9a6ea3a39e7f7bc77cbe6 Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 1 Sep 2025 15:46:07 +0200 Subject: [PATCH 063/102] test: add basic test for flakeModule functionality --- flake.nix | 4 +- nix/repo/flake.lock | 18 +++++++++ nix/repo/flake.nix | 2 + nix/repo/tests.nix | 10 +++++ .../fixtures/flake_parts}/flake.lock | 0 .../fixtures/flake_parts}/flake.nix | 6 +-- tests/flake_parts_test.nix | 39 +++++++++++++++++++ 7 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 nix/repo/tests.nix rename {flakeModuleTest => tests/fixtures/flake_parts}/flake.lock (100%) rename {flakeModuleTest => tests/fixtures/flake_parts}/flake.nix (92%) create mode 100644 tests/flake_parts_test.nix diff --git a/flake.nix b/flake.nix index ea47bb8..b507b22 100644 --- a/flake.nix +++ b/flake.nix @@ -21,14 +21,14 @@ cellBlocks = with ren.blocks; [ (simple "devShells") (simple "pkgs") - # (simple "tests") + (simple "tests") # (simple "docs") (simple "ci") ]; } { packages = ren.select self [ - # ["repo" "tests"] + ["repo" "tests"] # ["repo" "docs"] ["repo" "ci" "packages"] ["packages" "pkgs"] diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index 6b16df1..1b7c64c 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -17,9 +17,27 @@ "type": "gitlab" } }, + "nixtest-lib": { + "locked": { + "dir": "lib", + "lastModified": 1753957623, + "narHash": "sha256-kdImwKx57N0QL8HPUUb5ADwXFgSjaNOk39b/eKlzyTo=", + "owner": "TECHNOFAB", + "repo": "nixtest", + "rev": "22b43c9fe83be73c3f0648bbb54bc3c1cf7f96df", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "nixtest", + "type": "gitlab" + } + }, "root": { "inputs": { "devshell-lib": "devshell-lib", + "nixtest-lib": "nixtest-lib", "treefmt-nix": "treefmt-nix" } }, diff --git a/nix/repo/flake.nix b/nix/repo/flake.nix index 7d5e657..42077d0 100644 --- a/nix/repo/flake.nix +++ b/nix/repo/flake.nix @@ -1,6 +1,7 @@ { inputs = { devshell-lib.url = "gitlab:rensa-nix/devshell?dir=lib"; + nixtest-lib.url = "gitlab:TECHNOFAB/nixtest?dir=lib"; treefmt-nix = { url = "github:numtide/treefmt-nix"; flake = false; @@ -10,6 +11,7 @@ i // { devshell = i.devshell-lib.lib {inherit (i.parent) pkgs;}; + ntlib = i.nixtest-lib.lib {inherit (i.parent) pkgs;}; cilib = import "${i.parent.self}/lib" {inherit (i.parent) pkgs;}; treefmt = import i.treefmt-nix; }; diff --git a/nix/repo/tests.nix b/nix/repo/tests.nix new file mode 100644 index 0000000..62c0833 --- /dev/null +++ b/nix/repo/tests.nix @@ -0,0 +1,10 @@ +{inputs, ...}: let + inherit (inputs) pkgs ntlib cilib; +in { + tests = ntlib.mkNixtest { + modules = ntlib.autodiscover {dir = "${inputs.self}/tests";}; + args = { + inherit pkgs ntlib cilib; + }; + }; +} diff --git a/flakeModuleTest/flake.lock b/tests/fixtures/flake_parts/flake.lock similarity index 100% rename from flakeModuleTest/flake.lock rename to tests/fixtures/flake_parts/flake.lock diff --git a/flakeModuleTest/flake.nix b/tests/fixtures/flake_parts/flake.nix similarity index 92% rename from flakeModuleTest/flake.nix rename to tests/fixtures/flake_parts/flake.nix index 92d5c87..cca696f 100644 --- a/flakeModuleTest/flake.nix +++ b/tests/fixtures/flake_parts/flake.nix @@ -6,13 +6,11 @@ } @ inputs: flake-parts.lib.mkFlake {inherit inputs;} { imports = [ - ../lib/flakeModule.nix + "@repo_path@/lib/flakeModule.nix" ]; systems = import systems; flake = {}; - perSystem = { - ... - }: { + perSystem = _: { ci = { config = { # true is already default, just for testing diff --git a/tests/flake_parts_test.nix b/tests/flake_parts_test.nix new file mode 100644 index 0000000..f192420 --- /dev/null +++ b/tests/flake_parts_test.nix @@ -0,0 +1,39 @@ +{ + pkgs, + ntlib, + ... +}: { + suites."flake-parts" = { + pos = __curPos; + tests = [ + { + name = "flakeModule"; + type = "script"; + script = + # sh + '' + ${ntlib.helpers.scriptHelpers} + ${ntlib.helpers.path (with pkgs; [coreutils nix gnused gnugrep jq])} + repo_path=${../.} + + dir=$(mktemp -d) + trap "rm -rf $dir" EXIT + cd $dir + cp ${./fixtures/flake_parts}/* . + # import from the absolute path above, is easier than trying to figure out the repo path etc. + sed -i -e "s|@repo_path@|$repo_path|" flake.nix + + # NOTE: --impure is required since importing modules from absolute paths is not allowed in pure mode + nix build --impure .#gitlab-ci:pipeline:default + assert "-f result" "should exist" + assert_file_contains "result" "finalize_nix_ci" + jq '.' result # check if valid json just to be sure + + nix build --impure .#gitlab-ci:pipeline:default:pretty + assert "-f result" "should exist" + assert_file_contains "result" " - finalize_nix_ci" + ''; + } + ]; + }; +} From 6e4347af6952d1449c4d46af2409aa706619d230 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 10:35:35 +0200 Subject: [PATCH 064/102] chore(repo): add soonix to generate .gitlab-ci.yml --- .gitlab-ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++++ nix/repo/ci.nix | 53 +++++++++++++++++++++++++++++++++++++++++ nix/repo/devShells.nix | 12 ++++++++-- nix/repo/flake.lock | 18 ++++++++++++++ nix/repo/flake.nix | 2 ++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c6bc9f2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,54 @@ +# Generated by soonix, DO NOT EDIT +build:image: + after_script: + - install -D result dist/nix-ci-$ARCH.tar.gz + artifacts: + paths: + - dist + image: nixpkgs/nix-flakes:latest + parallel: + matrix: + - ARCH: + - x86_64-linux + - aarch64-linux + script: + - nix build .#image --system $ARCH + stage: build-images +deploy:image: + before_script: + - 'nix profile install nixpkgs#buildah + + export PATH="$PATH:$HOME/.nix-profile/bin" + + export REGISTRY_AUTH_FILE=${HOME}/auth.json + + echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin + $CI_REGISTRY + + mkdir -p /etc/containers && echo ''{"default":[{"type":"insecureAcceptAnything"}]}'' + > /etc/containers/policy.json + + mkdir -p /var/tmp + + ' + image: nixpkgs/nix-flakes:latest + needs: + - build:image + script: + - "export NORMALIZED_BRANCH=${CI_COMMIT_BRANCH/\\//-}\nbuildah manifest create localhost/nix-ci\n\ + buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-x86_64-linux.tar.gz\n\ + buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-aarch64-linux.tar.gz\n\ + buildah manifest push --all localhost/nix-ci docker://${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_SHORT_SHA}\n\ + # branches\nif [ -z \"$CI_COMMIT_TAG\" ]; then\n buildah manifest push --all\ + \ localhost/nix-ci docker://${CI_REGISTRY_IMAGE}/nix-ci:${NORMALIZED_BRANCH/main/latest}\n\ + fi\n# tags\nif [ -n \"$CI_COMMIT_TAG\" ]; then\n buildah manifest push --all\ + \ localhost/nix-ci docker://${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_TAG}\nfi\n" + stage: build-images +include: +- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA@$CI_COMMIT_SHORT_SHA + inputs: + version: $CI_COMMIT_SHORT_SHA +stages: +- build-images +- build +- trigger diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 913d8fc..9362a05 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -2,6 +2,59 @@ inherit (inputs) cilib; in cilib.mkCI { + config.soonix = { + componentUrl = "$CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA"; + componentVersion = "$CI_COMMIT_SHORT_SHA"; + # bootstrapping still needs to be done in the gitlab-ci.yml directly, + # the child pipeline can then use the built images to test them + extraData = { + stages = ["build-images" "build" "trigger"]; + "build:image" = { + stage = "build-images"; + parallel.matrix = [ + {ARCH = ["x86_64-linux" "aarch64-linux"];} + ]; + image = "nixpkgs/nix-flakes:latest"; + script = ["nix build .#image --system $ARCH"]; + after_script = ["install -D result dist/nix-ci-$ARCH.tar.gz"]; + artifacts.paths = ["dist"]; + }; + "deploy:image" = { + stage = "build-images"; + image = "nixpkgs/nix-flakes:latest"; + needs = ["build:image"]; + before_script = [ + # sh + '' + nix profile install nixpkgs#buildah + export PATH="$PATH:$HOME/.nix-profile/bin" + export REGISTRY_AUTH_FILE=''${HOME}/auth.json + echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + mkdir -p /etc/containers && echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json + mkdir -p /var/tmp + '' + ]; + script = [ + # sh + '' + export NORMALIZED_BRANCH=''${CI_COMMIT_BRANCH/\//-} + buildah manifest create localhost/nix-ci + buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-x86_64-linux.tar.gz + buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-aarch64-linux.tar.gz + buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:''${CI_COMMIT_SHORT_SHA} + # branches + if [ -z "$CI_COMMIT_TAG" ]; then + buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:''${NORMALIZED_BRANCH/main/latest} + fi + # tags + if [ -n "$CI_COMMIT_TAG" ]; then + buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:''${CI_COMMIT_TAG} + fi + '' + ]; + }; + }; + }; pipelines."default" = { stages = ["test" "build" "deploy"]; jobs = { diff --git a/nix/repo/devShells.nix b/nix/repo/devShells.nix index c754595..4a7da34 100644 --- a/nix/repo/devShells.nix +++ b/nix/repo/devShells.nix @@ -1,7 +1,13 @@ -{inputs, ...}: let - inherit (inputs) pkgs devshell treefmt; +{ + cell, + inputs, + ... +}: let + inherit (inputs) pkgs devshell treefmt soonix; + inherit (cell) ci; in { default = devshell.mkShell { + imports = [soonix.devshellModule]; packages = [ pkgs.nil (treefmt.mkWrapper pkgs { @@ -23,5 +29,7 @@ in { }; }) ]; + + soonix.hooks."ci" = ci.soonix; }; } diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index 1b7c64c..5483152 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -38,9 +38,27 @@ "inputs": { "devshell-lib": "devshell-lib", "nixtest-lib": "nixtest-lib", + "soonix-lib": "soonix-lib", "treefmt-nix": "treefmt-nix" } }, + "soonix-lib": { + "locked": { + "dir": "lib", + "lastModified": 1756797658, + "narHash": "sha256-4rkyP4oaoqG/FFVL7W8U+8hGer4tOBPff/2SeN5tJYQ=", + "owner": "TECHNOFAB", + "repo": "soonix", + "rev": "3baef660cf8b87391d475a0455dd66fae0e60008", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "soonix", + "type": "gitlab" + } + }, "treefmt-nix": { "flake": false, "locked": { diff --git a/nix/repo/flake.nix b/nix/repo/flake.nix index 42077d0..35542de 100644 --- a/nix/repo/flake.nix +++ b/nix/repo/flake.nix @@ -2,6 +2,7 @@ inputs = { devshell-lib.url = "gitlab:rensa-nix/devshell?dir=lib"; nixtest-lib.url = "gitlab:TECHNOFAB/nixtest?dir=lib"; + soonix-lib.url = "gitlab:TECHNOFAB/soonix?dir=lib"; treefmt-nix = { url = "github:numtide/treefmt-nix"; flake = false; @@ -11,6 +12,7 @@ i // { devshell = i.devshell-lib.lib {inherit (i.parent) pkgs;}; + soonix = i.soonix-lib.lib {inherit (i.parent) pkgs;}; ntlib = i.nixtest-lib.lib {inherit (i.parent) pkgs;}; cilib = import "${i.parent.self}/lib" {inherit (i.parent) pkgs;}; treefmt = import i.treefmt-nix; From 537d5c7c87ba921418c6540750be78dcfea31c99 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 10:36:46 +0200 Subject: [PATCH 065/102] chore: add LICENSE --- LICENSE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..57fd6fa --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2024 TECHNOFAB + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 46bb4fe455470971478b8db04955e1363bc203d6 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 10:48:39 +0200 Subject: [PATCH 066/102] chore: add examples for flake-parts and rensa-nix --- examples/flake-parts/flake.lock | 95 ++++++++++++++++++++++++++ examples/flake-parts/flake.nix | 39 +++++++++++ examples/rensa-nix/flake.lock | 63 +++++++++++++++++ examples/rensa-nix/flake.nix | 30 ++++++++ examples/rensa-nix/nix/repo/ci.nix | 18 +++++ examples/rensa-nix/nix/repo/flake.lock | 28 ++++++++ examples/rensa-nix/nix/repo/flake.nix | 10 +++ 7 files changed, 283 insertions(+) create mode 100644 examples/flake-parts/flake.lock create mode 100644 examples/flake-parts/flake.nix create mode 100644 examples/rensa-nix/flake.lock create mode 100644 examples/rensa-nix/flake.nix create mode 100644 examples/rensa-nix/nix/repo/ci.nix create mode 100644 examples/rensa-nix/nix/repo/flake.lock create mode 100644 examples/rensa-nix/nix/repo/flake.nix diff --git a/examples/flake-parts/flake.lock b/examples/flake-parts/flake.lock new file mode 100644 index 0000000..2e81bcf --- /dev/null +++ b/examples/flake-parts/flake.lock @@ -0,0 +1,95 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1756770412, + "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "4524271976b625a4a605beefd893f270620fd751", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nix-gitlab-ci": { + "locked": { + "dir": "lib", + "lastModified": 1752052838, + "narHash": "sha256-EqP4xB8YTVXWPCCchnVtQbuq0bKa79TUEcPF3hjuX/k=", + "owner": "TECHNOFAB", + "repo": "nix-gitlab-ci", + "rev": "0c6949f585a2c1ea2cf85fc01445496f7c75faae", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "nix-gitlab-ci", + "type": "gitlab" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1756696532, + "narHash": "sha256-6FWagzm0b7I/IGigOv9pr6LL7NQ86mextfE8g8Q6HBg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "58dcbf1ec551914c3756c267b8b9c8c86baa1b2f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754788789, + "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nix-gitlab-ci": "nix-gitlab-ci", + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/flake-parts/flake.nix b/examples/flake-parts/flake.nix new file mode 100644 index 0000000..c1b7977 --- /dev/null +++ b/examples/flake-parts/flake.nix @@ -0,0 +1,39 @@ +{ + outputs = { + flake-parts, + systems, + ... + } @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + inputs.nix-gitlab-ci.flakeModule + ]; + systems = import systems; + flake = {}; + perSystem = _: { + ci = { + pipelines = { + "default" = { + stages = ["example"]; + jobs."example" = { + stage = "example"; + script = ["echo hello world"]; + }; + }; + "test".jobs."example" = { + stage = ".pre"; + script = ["echo hello world"]; + }; + }; + }; + }; + }; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default-linux"; + # NOTE: better pin to a version + nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci?dir=lib"; + }; +} diff --git a/examples/rensa-nix/flake.lock b/examples/rensa-nix/flake.lock new file mode 100644 index 0000000..7c47f5c --- /dev/null +++ b/examples/rensa-nix/flake.lock @@ -0,0 +1,63 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1756542300, + "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754184128, + "narHash": "sha256-AjhoyBL4eSyXf01Bmc6DiuaMrJRNdWopmdnMY0Pa/M0=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "02e72200e6d56494f4a7c0da8118760736e41b60", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "ren": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "dir": "lib", + "lastModified": 1756370106, + "narHash": "sha256-l84ojcHuQWBwn4BRxQsMMfQpcq/Az/sHh/hSqFgVtyg=", + "owner": "rensa-nix", + "repo": "core", + "rev": "9c1a29fa9ba7cbbb78b9e47eb8afbcd29303a3b4", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "rensa-nix", + "repo": "core", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "ren": "ren" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/rensa-nix/flake.nix b/examples/rensa-nix/flake.nix new file mode 100644 index 0000000..1f81895 --- /dev/null +++ b/examples/rensa-nix/flake.nix @@ -0,0 +1,30 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + ren.url = "gitlab:rensa-nix/core?dir=lib"; + }; + + outputs = { + self, + ren, + ... + } @ inputs: + ren.buildWith + { + inherit inputs; + cellsFrom = ./nix; + transformInputs = system: i: + i + // { + pkgs = import i.nixpkgs {inherit system;}; + }; + cellBlocks = with ren.blocks; [ + (simple "ci") + ]; + } + { + packages = ren.select self [ + ["repo" "ci" "packages"] + ]; + }; +} diff --git a/examples/rensa-nix/nix/repo/ci.nix b/examples/rensa-nix/nix/repo/ci.nix new file mode 100644 index 0000000..adb707e --- /dev/null +++ b/examples/rensa-nix/nix/repo/ci.nix @@ -0,0 +1,18 @@ +{inputs, ...}: let + inherit (inputs) cilib; +in + cilib.mkCI { + pipelines = { + "default" = { + stages = ["example"]; + jobs."example" = { + stage = "example"; + script = ["echo hello world"]; + }; + }; + "test".jobs."example" = { + stage = ".pre"; + script = ["echo hello world"]; + }; + }; + } diff --git a/examples/rensa-nix/nix/repo/flake.lock b/examples/rensa-nix/nix/repo/flake.lock new file mode 100644 index 0000000..fb28eb7 --- /dev/null +++ b/examples/rensa-nix/nix/repo/flake.lock @@ -0,0 +1,28 @@ +{ + "nodes": { + "nix-gitlab-ci-lib": { + "locked": { + "dir": "lib", + "lastModified": 1752052838, + "narHash": "sha256-EqP4xB8YTVXWPCCchnVtQbuq0bKa79TUEcPF3hjuX/k=", + "owner": "TECHNOFAB", + "repo": "nix-gitlab-ci", + "rev": "0c6949f585a2c1ea2cf85fc01445496f7c75faae", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "nix-gitlab-ci", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "nix-gitlab-ci-lib": "nix-gitlab-ci-lib" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/rensa-nix/nix/repo/flake.nix b/examples/rensa-nix/nix/repo/flake.nix new file mode 100644 index 0000000..ad3b77d --- /dev/null +++ b/examples/rensa-nix/nix/repo/flake.nix @@ -0,0 +1,10 @@ +{ + inputs = { + nix-gitlab-ci-lib.url = "gitlab:TECHNOFAB/nix-gitlab-ci?dir=lib"; + }; + outputs = i: + i + // { + cilib = i.nix-gitlab-ci-lib.lib {inherit (i.parent) pkgs;}; + }; +} From aa1abf945e35f4e3cb1675515d596d3d0432e1c2 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 10:48:54 +0200 Subject: [PATCH 067/102] docs: add improved README.md --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e198879 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# Nix GitLab CI + +[![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org) +[![pipeline status](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/commits/main) +![License: MIT](https://img.shields.io/gitlab/license/technofab/nix-gitlab-ci) +[![Latest Release](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/releases) +[![Support me](https://img.shields.io/badge/Support-me-orange)](https://tec.tf/#support) +[![Docs](https://img.shields.io/badge/Read-Docs-orange)](https://nix-gitlab-ci.projects.tf) + +Flake module which allows generating a `.gitlab-ci.yml` from Nix. + +This allows easily using any Nix package in CI. + +Also makes it possible to split CI parts in a separate module which can be imported in multiple projects. + +## Usage (with flake-parts) + +```nix +# flake.nix +{ + ... + inputs.nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; # recommendation: pin to the latest release/version + + outputs = {...}: flake-parts.lib.mkFlake {...} { + imports = [ + inputs.nix-gitlab-ci.flakeModule + ]; + ... + + perSystem = {pkgs, ...}: { + ci = { + config = { + # configure Nix-GitLab-CI here, see docs for options + }; + pipelines."default" = { + stages = ["test"]; + jobs = { + "test" = { + stage = "test"; + nix.deps = [pkgs.unixtools.ping]; + script = [ + "ping -c 5 8.8.8.8" + ]; + }; + }; + }; + # runs on a merge request for example + pipelines."merge_request_event" = { + stages = ["some_stage"]; + jobs = { ... }; + }; + }; + ... + } + } +} +``` + +Now either use this in your .gitlab-ci.yml or setup Soonix to auto generate this +file for you with the right version (see the [docs][docs-soonix] for more). + +```yaml +# .gitlab-ci.yml +include: + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: pin to the latest release/version (don't use "main" etc.) + inputs: + version: # docker image tag, use the same version as a above +``` + +## Usage (directly) + +```nix +let + cilib = inputs.nix-gitlab-ci.lib {inherit pkgs;}; +in + cilib.mkCI { + config = ...; + pipelines."default" = ...; + }; + # exposes `soonix` for the soonix hook and `packages` which contain the configs, jobs etc. +``` + +## Utilities + +### Disable Caching temporarily + +To disable any of the provided caches for a pipeline one can set `NIX_CI_DISABLE_CACHE` to +anything non-empty (eg. "yes") when triggering the pipeline. + +The `build:nix-ci` job has a different special environment variable `NIX_CI_FORCE_BUILD` +(useful if the generated pipeline in the cache is outdated, this will build it again). + +### Run Jobs locally + +You can run any job's script (+ before and after) locally with Nix for easier testing: + +```sh +# / pipeline name, like "default" +nix run .#gitlab-ci:pipeline::job: +``` + +There is also `.#gitlab-ci:pipeline::job-deps:` which generates and exports the required environment variables for each job: + +- PATH (with all deps) +- any custom env variables which contain store paths to not break stuff when switching archs + +## Thanks to + +Some parts of this implementation are adapted/inspired from https://gitlab.com/Cynerd/gitlab-ci-nix + +[docs-soonix]: https://nix-gitlab-ci.projects,tf/soonix "Soonix Integration" From e7f7043012b33455db83bfcdf701e8b069bc1a7c Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 11:00:38 +0200 Subject: [PATCH 068/102] docs: add initial docs setup --- docs/index.md | 2 ++ docs/options.md | 3 ++ flake.nix | 4 +-- lib/impl/helpers.nix | 9 ++++-- nix/repo/docs.nix | 68 +++++++++++++++++++++++++++++++++++++++++++- nix/repo/flake.lock | 18 ++++++++++++ nix/repo/flake.nix | 2 ++ 7 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 docs/index.md create mode 100644 docs/options.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..07a2ade --- /dev/null +++ b/docs/index.md @@ -0,0 +1,2 @@ +# Nix-GitLab-CI + diff --git a/docs/options.md b/docs/options.md new file mode 100644 index 0000000..4ca74a4 --- /dev/null +++ b/docs/options.md @@ -0,0 +1,3 @@ +# Options + +{% include 'options.md' %} diff --git a/flake.nix b/flake.nix index b507b22..769a6b9 100644 --- a/flake.nix +++ b/flake.nix @@ -22,14 +22,14 @@ (simple "devShells") (simple "pkgs") (simple "tests") - # (simple "docs") + (simple "docs") (simple "ci") ]; } { packages = ren.select self [ ["repo" "tests"] - # ["repo" "docs"] + ["repo" "docs"] ["repo" "ci" "packages"] ["packages" "pkgs"] ]; diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 38dc030..147788c 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -2,7 +2,7 @@ pkgs, lib, } @ args: let - inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType; + inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType literalExpression; in rec { prepend = key: arr: job: { ${key} = arr ++ (job.${key} or []); @@ -61,12 +61,17 @@ in rec { _type = "unset"; }; isUnset = isType "unset"; - unsetOr = types.either unsetType; + unsetOr = typ: + (types.either unsetType typ) + // { + description = typ.description; + }; mkUnsetOption = opts: mkOption (opts // { type = unsetOr opts.type; default = opts.default or unset; + defaultText = literalExpression "unset"; }); filterUnset = value: diff --git a/nix/repo/docs.nix b/nix/repo/docs.nix index 0967ef4..d82a071 100644 --- a/nix/repo/docs.nix +++ b/nix/repo/docs.nix @@ -1 +1,67 @@ -{} +{inputs, ...}: let + inherit (inputs) pkgs cilib doclib; + + optionsDoc = doclib.mkOptionDocs { + module = cilib.modules.nixCiSubmodule; + roots = [ + { + url = "https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/blob/main/lib"; + path = "${inputs.self}/lib"; + } + ]; + }; + optionsDocs = pkgs.runCommand "options-docs" {} '' + mkdir -p $out + ln -s ${optionsDoc} $out/options.md + ''; +in + (doclib.mkDocs { + docs."default" = { + base = "${inputs.self}"; + path = "${inputs.self}/docs"; + material = { + enable = true; + colors = { + primary = "deep orange"; + accent = "orange"; + }; + umami = { + enable = true; + src = "https://analytics.tf/umami"; + siteId = "28f7c904-db22-4c2b-9ee4-ed42e14b6db9"; + domains = ["nix-gitlab-ci.projects.tf"]; + }; + }; + macros = { + enable = true; + includeDir = toString optionsDocs; + }; + config = { + site_name = "Nix-GitLab-CI"; + repo_name = "TECHNOFAB/nix-gitlab-ci"; + repo_url = "https://gitlab.com/TECHNOFAB/nix-gitlab-ci"; + theme = { + logo = "images/logo.png"; + icon.repo = "simple/gitlab"; + favicon = "images/favicon.png"; + }; + nav = [ + {"Introduction" = "index.md";} + {"Options" = "options.md";} + ]; + markdown_extensions = [ + { + "pymdownx.highlight".pygments_lang_class = true; + } + "pymdownx.inlinehilite" + "pymdownx.snippets" + "pymdownx.superfences" + "pymdownx.escapeall" + "fenced_code" + ]; + }; + }; + }).packages + // { + inherit optionsDocs; + } diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index 5483152..0463608 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -17,6 +17,23 @@ "type": "gitlab" } }, + "nixmkdocs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1755785622, + "narHash": "sha256-xBb9PCkszmrWSEqUiPC7oBJABm1thF572S5QHtloZ+M=", + "owner": "TECHNOFAB", + "repo": "nixmkdocs", + "rev": "61da605a9bff12f66c4b743f43aea59ca200f533", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "nixmkdocs", + "type": "gitlab" + } + }, "nixtest-lib": { "locked": { "dir": "lib", @@ -37,6 +54,7 @@ "root": { "inputs": { "devshell-lib": "devshell-lib", + "nixmkdocs-lib": "nixmkdocs-lib", "nixtest-lib": "nixtest-lib", "soonix-lib": "soonix-lib", "treefmt-nix": "treefmt-nix" diff --git a/nix/repo/flake.nix b/nix/repo/flake.nix index 35542de..149ae74 100644 --- a/nix/repo/flake.nix +++ b/nix/repo/flake.nix @@ -3,6 +3,7 @@ devshell-lib.url = "gitlab:rensa-nix/devshell?dir=lib"; nixtest-lib.url = "gitlab:TECHNOFAB/nixtest?dir=lib"; soonix-lib.url = "gitlab:TECHNOFAB/soonix?dir=lib"; + nixmkdocs-lib.url = "gitlab:TECHNOFAB/nixmkdocs?dir=lib"; treefmt-nix = { url = "github:numtide/treefmt-nix"; flake = false; @@ -14,6 +15,7 @@ devshell = i.devshell-lib.lib {inherit (i.parent) pkgs;}; soonix = i.soonix-lib.lib {inherit (i.parent) pkgs;}; ntlib = i.nixtest-lib.lib {inherit (i.parent) pkgs;}; + doclib = i.nixmkdocs-lib.lib {inherit (i.parent) pkgs;}; cilib = import "${i.parent.self}/lib" {inherit (i.parent) pkgs;}; treefmt = import i.treefmt-nix; }; From cad40720a6f824894070525aaadd57b731e42482 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 11:12:29 +0200 Subject: [PATCH 069/102] docs: improve option descriptions --- docs/images/favicon.png | Bin 0 -> 1995 bytes docs/images/logo.png | Bin 0 -> 40993 bytes lib/default.nix | 4 +-- lib/impl/modules/job.nix | 45 ++++++++++++++++++++++++++++++---- lib/impl/modules/pipeline.nix | 20 ++++++++++----- lib/impl/modules/root.nix | 29 ++++++++++++++++------ lib/impl/modules/soonix.nix | 16 +++++++++--- nix/repo/docs.nix | 1 + 8 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 docs/images/favicon.png create mode 100644 docs/images/logo.png diff --git a/docs/images/favicon.png b/docs/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..135b03026d6a5ffb7cbeec80c97224e5b6eb776d GIT binary patch literal 1995 zcmV;+2Q>JJP)Ilb@!-~pgw+p^fd`_57-dFY%EX0E7d_G7}<8 z{n2aqBQ$P)exR!E0QLd}$?1jt8xb1_N?<%jn!WaNwNK(1q7lBs^1kPvDpBkHxAB)z>XxxIyZ|R}wo{HJ=6Dn{QP$^KDk_0i-n|xe#zGce`^2N_Ptw zDEXr1=DW_~F|+p=Ci*yHh#-gvi1COZqY!b4$Pi#E$TNu4h`6^C5qiZ&jdwT$=As_6 zp(v|?4?(}kOzKkXFF;3A6Q-I!0NE&R1t=hx=O$kKqR-&6cb0>(BJJT-(IV_~8%7XI z?yFBu|FPH5s1u-Ft>t^CWj(!CwC+VNWX2%Vhj9s@l!Lmhp3vrv0RDQ zt){tQaM?SFz+^CIqPn@kyfv<0t9!QMeB~yu1ywhR)=dW{0naw{p}YjR#-|qs-9ZfM z&s5EBKFU=v{{T#}ghmgo{$`)5S&dE3`rC2xKW86DwGgGYbxqYCV7#?%immwfwBqtEUqHey!46|F!zx?@zlaR@^9f%hE^4G)@= z^2Sq(K|k`Q8}yIbU-Wr$df`rBw8gJb=#{f9_RMW#@GsdD{?r)%xA8Hr`dEF!&NoL+ zzg3WDynmt?be4}%rY5HsZU9Ev_!R<`-@444da{?piXl6VEsc+H+-lwPD-%nBWFPj# z#PZ`MK!uI>;i7Hb?n3J9-W$Lj%ylRy z7w#B8^U61srL|Wi$a9^#jROT2swl0g#lwEEMG%7`LOE1k!{IAeN$e0#haksIT7z2X z^^o=v-G1D9$!xfCAcRvjwY>FpB~P>syMfDquy#sVYm)#6PUTXPs(Aq9r-E3; zUO=QL%3L7R12D7^V4j~dKoo!x0O4&-LR!M5(*~C>)dR4+M;jiE2)$+Pa)u}o_bZM- zwaJ-Ls`|BfZz`{5M5w+IgQ{7;aFq6-#*G_*u0RW*tU=&9pr=0-k74zxJGb4D@d(kAlMmb!s<`2n2`e zZ8Iw(Qfb}MY-+k-#~UO50c77%yY2L7q(OyW_u?jKG7y>%eP1qlMo|A}T9U zicnpZIkuZ`d%h3Ym~eUS#NxUku|)A_qOH$$-<`VxUD$mtEOBsJ-|b_pL~GxO$|DME z{Z5is#zbFxdE2q+ihkC}^e+G>FneazkRff022I@9>CKOTB|i7=bs%F=t1TPLTLx|h zhBo;*1`F1v#?;=`(+5D@WLIC{pB6w5T?D4(j>tMI=zCVja~gPV*43N?qQ()RyA8}w zfKToHPIhX{XLtQJ6F1o}Xr2{w9{SKQ z>f_d=Jf!~UHSWNGu2qsc+4%8!>r?aW>u0!%#sWDueI-Dj?9`Z==CWX{4IDroVrJo0;=pnpU}rrFnLr}kE0 zo|V}EpCasS)(6bE$*xE%8!PkHrylZ|>k9M)>a9_pw?6gXzI)7Y6+H}usd94d3o&K) d-jDAQ@IM<&9OUDTXUhNp002ovPDHLkV1kV3&P@OS literal 0 HcmV?d00001 diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2a70bf66f5680e7b7c454e0a850d5ce731562603 GIT binary patch literal 40993 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 Date: Tue, 2 Sep 2025 11:34:24 +0200 Subject: [PATCH 070/102] chore: reformat --- README.md | 2 +- lib/default.nix | 5 ++++- lib/impl/helpers.nix | 2 +- lib/impl/modules/root.nix | 2 +- nix/repo/devShells.nix | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e198879..1570632 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Also makes it possible to split CI parts in a separate module which can be impor } ``` -Now either use this in your .gitlab-ci.yml or setup Soonix to auto generate this +Now either use this in your .gitlab-ci.yml or setup Soonix to auto generate this file for you with the right version (see the [docs][docs-soonix] for more). ```yaml diff --git a/lib/default.nix b/lib/default.nix index 5203efe..ff3ec88 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -10,7 +10,10 @@ args: let cilib = { inherit (impl) helpers modules mkPipeline mkJobRun mkJobDeps mkJobPatched; utils = import ./utils.nix {inherit pkgs;}; - version = trimWith { start = true; end = true; } (builtins.readFile ./VERSION); + version = trimWith { + start = true; + end = true; + } (builtins.readFile ./VERSION); mkCI = config: (evalModules { diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 147788c..ebeeb46 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -64,7 +64,7 @@ in rec { unsetOr = typ: (types.either unsetType typ) // { - description = typ.description; + inherit (typ) description; }; mkUnsetOption = opts: mkOption (opts diff --git a/lib/impl/modules/root.nix b/lib/impl/modules/root.nix index db7e19a..84fb5ba 100644 --- a/lib/impl/modules/root.nix +++ b/lib/impl/modules/root.nix @@ -18,7 +18,7 @@ in rec { nixJobsByDefault = mkOption { description = '' Whether to transform all jobs to nix-configured jobs by default. - If false, you need to set [`nix.enable`](#pipelinesnamejobsnamenixenable) + If false, you need to set [`nix.enable`](#pipelinesnamejobsnamenixenable) for each job you want to be transformed. ''; type = types.bool; diff --git a/nix/repo/devShells.nix b/nix/repo/devShells.nix index 4a7da34..10889f9 100644 --- a/nix/repo/devShells.nix +++ b/nix/repo/devShells.nix @@ -19,7 +19,7 @@ in { yamlfmt.enable = true; }; settings.formatter = { - yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; + yamlfmt.excludes = ["templates/nix-gitlab-ci.yml" ".gitlab-ci.yml"]; mdformat.command = let pkg = pkgs.python3.withPackages (p: [ p.mdformat From 0bd75fd1bb9b60ebebfb810600b9cb0fdb988967 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 11:49:35 +0200 Subject: [PATCH 071/102] docs: add remaining docs back with some additions/improvements --- docs/caching.md | 50 ++++++++++++++++ docs/cicd_component.md | 41 +++++++++++++ docs/environment_variables.md | 107 ++++++++++++++++++++++++++++++++++ docs/examples.md | 13 +++++ docs/index.md | 11 +++- docs/kubernetes_runner.md | 38 ++++++++++++ docs/multi_pipeline.md | 31 ++++++++++ docs/setup.md | 81 +++++++++++++++++++++++++ docs/soonix.md | 20 +++++++ docs/usage.md | 73 +++++++++++++++++++++++ docs/utilities.md | 64 ++++++++++++++++++++ nix/repo/docs.nix | 10 ++++ 12 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 docs/caching.md create mode 100644 docs/cicd_component.md create mode 100644 docs/environment_variables.md create mode 100644 docs/examples.md create mode 100644 docs/kubernetes_runner.md create mode 100644 docs/multi_pipeline.md create mode 100644 docs/setup.md create mode 100644 docs/soonix.md create mode 100644 docs/usage.md create mode 100644 docs/utilities.md diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 0000000..0a84110 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,50 @@ +# Caching + +Nix GitLab CI supports several caching mechanisms to speed up your pipelines. + +## GitLab Runner Cache + +The runner cache strategy copies the new store paths into a directory `.nix-cache`, +which is then saved in the regular GitLab cache (technically runner cache). +It's also configured as a substituter automatically. + +To enable, set the cache strategy to `runner`. + +Configure it using these environment variables: + +- `RUNNER_CACHE`: path to the runner cache (default `.nix-cache`) + +!!! warning + + This is very inefficient and should probably only be used for very very small + dependency counts. Otherwise it takes an eternity to save to cache. + +## Cachix + +Cachix is a hosted binary cache service that can significantly speed up Nix +builds by sharing build results. + +To enable, set the cache strategy to `attic`. + +Configure it using these environment variables: + +- `CACHIX_CACHE`: name of the cache to use +- (`CACHIX_AUTH_TOKEN`): cachix client itself uses this for authentication + +!!! warning + + Cachix has not been tested. Feedback is appreciated :) + +## Attic (Self-Hosted Cache) + +Attic is a self-hosted, deduplicating binary cache. It's a great option if you +want more control over your caching infrastructure and to have the cache closer +to your runners. + +To enable, set the cache strategy to `attic`. + +Configure it using these environment variables: + +- `ATTIC_SERVER`: URL of the server +- `ATTIC_CACHE`: name of the cache to use +- `ATTIC_TOKEN`: auth token from the attic server diff --git a/docs/cicd_component.md b/docs/cicd_component.md new file mode 100644 index 0000000..2fe2ae2 --- /dev/null +++ b/docs/cicd_component.md @@ -0,0 +1,41 @@ +# CI/CD Component + +The CI/CD Component has some inputs which configure defaults for Nix GitLab CI. + +## `version` + +- Type: `string` + +Which version of the Nix CI image to use. Using a tag/version is recommended. +Will not do anything if a custom image is specified using `NIX_CI_IMAGE`. + +## `cache_strategy` + +- Type: `string` +- Default: `"auto"` +- Options: `auto` | `none` | `runner` | `cachix` | `attic` + +Sets the default caching strategy. + +- `auto`: dynamically selects the best strategy for every job based on env variables +- `none`: disables caching +- `runner`, `cachix` & `attic`: forces every job to use this strategy + +Can be overridden by `NIX_CI_CACHE_STRATEGY`, see [Environment Variables](./environment_variables.md). + +## `cache_files` + +- Type: `array` (of strings) +- Default: `["flake.nix", "flake.lock"]` + +Files to use as the cache key for the generated pipeline yaml. +If you use a file like `ci.nix` to define CI, add that here for example. +This makes sure that changes to your Nix CI configuration will invalidate the cache, +otherwise an old pipeline yaml might be used. + +!!! warning + + The value of this is used in `cache:key:files`, which currently only supports + a max of 2 entries. So use something like `["flake.*", "ci.nix"]` to match + `flake.lock`, `flake.nix` and `ci.nix`. + See [gitlab-org/gitlab#301161](https://gitlab.com/gitlab-org/gitlab/-/issues/301161) diff --git a/docs/environment_variables.md b/docs/environment_variables.md new file mode 100644 index 0000000..56c0ba5 --- /dev/null +++ b/docs/environment_variables.md @@ -0,0 +1,107 @@ +# Environment Variables + +Nix GitLab CI is mostly controlled using environment variables. +This page outlines all the variables and their use case. + +## `NIX_CI_IMAGE` + +| | | +| ----------- | -------------------------------------------------------------------------- | +| Default | `registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci@$[[ inputs.version ]]` | +| Description | Image to use for the jobs | + +## `NIX_CI_PIPELINE_NAME` + +| | | +| ----------- | ------------------------------------------------- | +| Default | N/A | +| Description | Explicitly request a pipeline to be built and ran | +| See also | [Multi Pipeline](./multi_pipeline.md) | + +## `NIX_CI_DEFAULT_SOURCES` + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Default | `.*` | +| Description | Regex to match `$CI_PIPELINE_SOURCE` against. If it matches, the `default` pipeline will be ran, otherwise `$CI_PIPELINE_SOURCE` | +| See also | [Multi Pipeline](./multi_pipeline.md) | + +## `NIX_CI_FORCE_BUILD` + +| | | +| ----------- | -------------------------------------------------------------------------------------- | +| Default | N/A | +| Description | Set to any non-empty value to force the `nix-ci:build` job to freshly build the config | +| See also | [Caching](./caching.md) | + +## `NIX_CI_DISABLE_CACHE` + +| | | +| ----------- | ------------------------------------------------------ | +| Default | N/A | +| Description | Set to any non-empty value to disable caching for jobs | +| See also | [Caching](./caching.md) | + +## `NIX_CI_CACHE_STRATEGY` + +| | | +| ----------- | --------------------------------------------------------------------------------- | +| Default | `$[[ inputs.cache_strategy ]]` -> defaults to `auto` | +| Description | Caching strategy to use. `auto` will select the strategy based on runner settings | +| See also | [Caching](./caching.md) | + +## `NIX_CI_RUNNER_CACHE_STRATEGY` + +| | | +| ----------- | ---------------------------------------------------------------- | +| Default | N/A | +| Description | Every runner can set it's own preferred cache strategy with this | +| See also | [Caching](./caching.md) | + +## `NIX_CI_DEFAULT_CACHE_STRATEGY` + +| | | +| ----------- | ------------------------------------------------------------------------------------------------- | +| Default | `none` | +| Description | If no runner cache strategy is set and the main strategy is set to auto, this will be the default | +| See also | [Caching](./caching.md) | + +## `RUNNER_CACHE` + +| | | +| ----------- | -------------------------------------- | +| Default | `.nix-cache` | +| Description | Path to directory for the runner cache | +| See also | [Caching](./caching.md) | + +## `CACHIX_CACHE` + +| | | +| ----------- | ------------------------------- | +| Default | N/A | +| Description | Name of the cachix cache to use | +| See also | [Caching](./caching.md) | + +## `ATTIC_CACHE` + +| | | +| ----------- | ------------------------------ | +| Default | N/A | +| Description | Name of the attic cache to use | +| See also | [Caching](./caching.md) | + +## `ATTIC_SERVER` + +| | | +| ----------- | ----------------------- | +| Default | N/A | +| Description | URL of the attic server | +| See also | [Caching](./caching.md) | + +## `ATTIC_TOKEN` + +| | | +| ----------- | ------------------------------- | +| Default | N/A | +| Description | API token from the attic server | +| See also | [Caching](./caching.md) | diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..336a55a --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,13 @@ +# Example Configs + +- [TECHNOFAB/nix-gitlab-ci](https://gitlab.com/TECHNOFAB/nix-gitlab-ci) +- [TECHNOFAB/nixlets](https://gitlab.com/TECHNOFAB/nixlets) +- [TECHNOFAB/nixible](https://gitlab.com/TECHNOFAB/nixible) +- [TECHNOFAB/nixmkdocs](https://gitlab.com/TECHNOFAB/nixmkdocs) +- [TECHNOFAB/tofunix](https://gitlab.com/TECHNOFAB/tofunix) +- [TECHNOFAB/nixtest](https://gitlab.com/TECHNOFAB/nixtest) + +!!! note + + Feel free to edit this page and add your project if you're using + Nix GitLab CI :) diff --git a/docs/index.md b/docs/index.md index 07a2ade..214fbf3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,2 +1,11 @@ -# Nix-GitLab-CI +# Nix GitLab CI +This project provides a Nix flake module that allows you to generate your `.gitlab-ci.yml` file directly from your Nix configuration. + +## Features + +- **Reproducibility:** Leverage Nix's strength in creating reproducible environments for your CI jobs. +- **Easy Dependency Management:** Easily include any package available in Nixpkgs or your own defined packages within your CI jobs using Nix. +- **Modularity:** Define and manage your CI configurations in a structured and modular way using Nix modules, making it easier to share and reuse CI logic across multiple projects. + +This documentation will guide you through setting up and using Nix GitLab CI for your projects. diff --git a/docs/kubernetes_runner.md b/docs/kubernetes_runner.md new file mode 100644 index 0000000..4c20421 --- /dev/null +++ b/docs/kubernetes_runner.md @@ -0,0 +1,38 @@ +# Kubernetes Runner Setup + +Using the GitLab Kubernetes runner allows your CI jobs to run as pods in a Kubernetes cluster. +Nix GitLab CI can be integrated with this setup, and using advanced configuration options like +`pod_spec` makes it easy to add runner specific caching. + +Using this Runner configuration ... + +```toml +[[runners.kubernetes.pod_spec]] +name = "nix-ci-cache-secrets" +patch = ''' + containers: + - name: build + envFrom: + - secretRef: + name: nix-ci-cache-env +''' +``` + +... and a secret containing ... + +```yaml +NIX_CI_RUNNER_CACHE_STRATEGY: attic +ATTIC_SERVER: # example: http://atticd..svc.cluster.local:8080 +ATTIC_CACHE: ci # name however you want, just needs to exist +ATTIC_TOKEN: +``` + +... makes your jobs automatically cache their Nix store paths to the in-cluster +attic when running with this runner. + +Other runners could use cachix or no cache, you get the idea ;P + +!!! note + + This of course works with any executor where you can set environment + variables. This is just an example how to do it in Kubernetes easily. diff --git a/docs/multi_pipeline.md b/docs/multi_pipeline.md new file mode 100644 index 0000000..d8dbfe6 --- /dev/null +++ b/docs/multi_pipeline.md @@ -0,0 +1,31 @@ +# Multiple Pipelines + +With V2, Nix GitLab CI can generate different pipelines, depending on the +pipeline source (`$CI_PIPELINE_SOURCE`). + +By default, no matter which source, the `default` pipeline is built and ran. +`$NIX_CI_PIPELINE` can override that, eg. when manually triggering a run. +To configure which source should be 1-to-1 translated to a pipeline with the +same name, set `$NIX_CI_DEFAULT_SOURCES` to a regex which explicitly does not +match these sources. Or set it to an impossible to match regex, then it will +always run the pipeline named after `$CI_PIPELINE_SOURCE`. + +## Example 1: always run default + +If you only have a single pipeline, you just have to call it `default`. +Everything else works out of the box. + +## Example 2: default and merge_request_event + +If you want the source `merge_request_event` to trigger a different pipeline, +name it like that and set `$NIX_CI_DEFAULT_SOURCES` to `^(merge_request_event)$`. +Now a merge request will run this pipeline, while everything else runs `default`. + +## Example 3: default, push and web + +Set `$NIX_CI_DEFAULT_SOURCES` to `^(push|web)$`. + +## Example 4: always run the specific pipelines, never default + +Set `$NIX_CI_DEFAULT_SOURCES` to any regex that never matches the sources, +like `a\A` or `nothing`. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..c1f85df --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,81 @@ +# Setup + +To integrate Nix GitLab CI into your project, you need to make two main changes: + +1. Add the `nix-gitlab-ci` flake module to your `flake.nix`. +1. Include the necessary component in your `.gitlab-ci.yml`. + +## Adding to `flake.nix` + +In your project's `flake.nix`, add `nix-gitlab-ci` as an input and import its +flake module within your `flake-parts` configuration. + +```nix title="flake.nix" +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Or your preferred nixpkgs branch/version + flake-parts.url = "github:hercules-ci/flake-parts"; + + # Add nix-gitlab-ci as an input + # recommendation: pin to a specific release/version + nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; + }; + + outputs = { nixpkgs, flake-parts, ... }@inputs: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + # Import the nix-gitlab-ci flake module + inputs.nix-gitlab-ci.flakeModule + ]; + + systems = [ + "x86_64-linux" + "aarch64-linux" + # Add other systems you need + ]; + + perSystem = { pkgs, ... }: { + # define your CI pipelines + # ci.pipelines."merge_request_event" = { ... }; + }; + }; +} +``` + +Replace `` with the specific version or commit hash of `nix-gitlab-ci` +you wish to use. Pinning to a specific version is highly recommended for +reproducibility and compatibility. + +!!! warning + + While the flake input is locked through `flake.lock`, the CI/CD component + will always use the latest commit of the reference. This means that by using + a branch like `main` as version for both, the CI/CD component will always use + the latest commit while your flake uses a fixed one. + This could result in drift between both, potentially breaking stuff. + +## Including in `.gitlab-ci.yml` + +Your `.gitlab-ci.yml` file will be minimal. Its primary role is to include the +`nix-gitlab-ci` component, which will then generate the full CI configuration +based on your Nix code. + +```yaml title=".gitlab-ci.yml" +include: + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ + inputs: + # This input sets the Docker image tag used for the CI jobs. + # Use the same version as you pinned in your flake.nix for consistency. + version: +``` + +Again, ensure `` matches the version used in your `flake.nix`. +This component includes a job (`build:nix-ci`) that will evaluate your Nix +configuration and generate the `.gitlab-ci.yml` used for the pipeline run. + +!!! note + + Since V3 [Soonix](https://soonix.projects.tf) is supported, this can + automatically generate the `.gitlab-ci.yml` for you, with the version + automatically following the flake. + See [Soonix Integration](./soonix.md) for more. diff --git a/docs/soonix.md b/docs/soonix.md new file mode 100644 index 0000000..b333ab6 --- /dev/null +++ b/docs/soonix.md @@ -0,0 +1,20 @@ +# Soonix Integration + +[Soonix](https://soonix.projects.tf) can be used to automatically generate the +`.gitlab-ci.yml` for you. +This will by default include the CI/CD component with the same version as the +flake (using the `VERSION` file in this repo). + +You can specify some options to configure this, like changing the component URL +or adding extra data to your `.gitlab-ci.yml` (like this repo does to bootstrap +Nix-GitLab-CI). See [Options](./options.md#configsoonix) for all the config options. + +You can use it like this: + +```nix +let + ci = cilib.mkCI { ... }; +in { + soonix.hooks."ci" = ci.soonix; +} +``` diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..44c0bf7 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,73 @@ +# Usage + +## Usage (with flake-parts) + +```nix +# flake.nix +{ + ... + inputs.nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; # recommendation: pin to the latest release/version + + outputs = {...}: flake-parts.lib.mkFlake {...} { + imports = [ + inputs.nix-gitlab-ci.flakeModule + ]; + ... + + perSystem = {pkgs, ...}: { + ci = { + config = { + # configure Nix-GitLab-CI here, see docs for options + }; + pipelines."default" = { + stages = ["test"]; + jobs = { + "test" = { + stage = "test"; + nix.deps = [pkgs.unixtools.ping]; + script = [ + "ping -c 5 8.8.8.8" + ]; + }; + }; + }; + # runs on a merge request for example + pipelines."merge_request_event" = { + stages = ["some_stage"]; + jobs = { ... }; + }; + }; + ... + } + } +} +``` + +Now either use this in your .gitlab-ci.yml or setup Soonix to auto generate this +file for you with the right version (see the [docs][docs-soonix] for more). + +```yaml +# .gitlab-ci.yml +include: + - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: pin to the latest release/version (don't use "main" etc.) + inputs: + version: # docker image tag, use the same version as a above +``` + +## Usage (directly) + +```nix +let + cilib = inputs.nix-gitlab-ci.lib {inherit pkgs;}; +in + cilib.mkCI { + config = ...; + pipelines."default" = ...; + }; + # exposes `soonix` for the soonix hook and `packages` which contain the configs, jobs etc. +``` + +______________________________________________________________________ + +Since V2 multiple pipelines are supported. +See [Multiple Pipelines](./multi_pipeline.md) for more. diff --git a/docs/utilities.md b/docs/utilities.md new file mode 100644 index 0000000..b82091d --- /dev/null +++ b/docs/utilities.md @@ -0,0 +1,64 @@ +# Utilities + +Nix GitLab CI provides a couple of utilities to help with development and +debugging. + +## Disabling caching temporarily + +Nix GitLab CI often utilizes caching mechanisms to speed up your pipelines +(see [Caching](./caching.md)). +However, there might be situations where you need to temporarily disable these +caches for a specific pipeline run, for example, to debug a caching issue or +ensure a clean build. + +To disable most of the provided caches for a pipeline, set the environment +variable `NIX_CI_DISABLE_CACHE` to any non-empty value (e.g., `yes`, `true`, `1`) +when triggering the pipeline in the GitLab UI or via the API. + +## Forcing a rebuild of the CI pipeline definition + +The job responsible for generating the `.gitlab-ci.yml` from your Nix code +(`build:nix-ci`) might itself be cached. If you've made changes to your Nix CI +configuration and the pipeline doesn't seem to pick them up, the cached job +definition might be the reason. + +You should first double check if all the Nix files you defined the CI config in +are specified in the `cache_files` CI/CD-component input +(see [CI/CD Component](./cicd_component.md) for more). + +To force this specific job to rebuild and re-evaluate your Nix configuration, +set the environment variable `NIX_CI_FORCE_BUILD` when triggering the pipeline. + +## Running jobs locally + +One of the benefits of defining your CI jobs with Nix is the ability to run them +locally in an environment that closely mirrors the CI environment. This can +significantly speed up debugging and development. + +You can run the script of any defined job locally using the `nix run` command. +The syntax is: + +```sh +nix run .#gitlab-ci:pipeline::job: +``` + +Replace `` with the name of the pipeline the job belongs to +(e.g., `default` for jobs defined under the `ci` attribute) and `` +with the name of the job you want to run. + +This command will set up the environment with the specified `nix.deps` and +execute the job's `script`. + +There is also an attribute `.#gitlab-ci:pipeline::job-deps:`. +Building this derivation will generate a shell script which exports the required +environment variables for the job, such as the `PATH` including all dependencies +and any custom environment variables that contain store paths (ensuring they are +correctly resolved across different architectures). + +You can use this to inspect the environment that would be set up for a job without +running the full script. + +## Viewing generated config in YAML + +Since the GitLab CI config is generated simply using JSON, it's hard to read and +debug. For debugging V3 now adds another package `gitlab-ci:pipeline::pretty`. diff --git a/nix/repo/docs.nix b/nix/repo/docs.nix index dad03e1..bb05f74 100644 --- a/nix/repo/docs.nix +++ b/nix/repo/docs.nix @@ -47,6 +47,16 @@ in }; nav = [ {"Introduction" = "index.md";} + {"Setup" = "setup.md";} + {"Usage" = "usage.md";} + {"CI/CD Component" = "cicd_component.md";} + {"Environment Variables" = "environment_variables.md";} + {"Caching" = "caching.md";} + {"Multiple Pipelines" = "multi_pipeline.md";} + {"Soonix Integration" = "soonix.md";} + {"Utilities" = "utilities.md";} + {"Kubernetes Runner Example" = "kubernetes_runner.md";} + {"Example Configs" = "examples.md";} {"Options" = "options.md";} ]; markdown_extensions = [ From 436e2fde2527fa9803df3adb6ebef930526be893 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 12:08:29 +0200 Subject: [PATCH 072/102] test: copy over most tests, add new tests for soonix --- lib/impl/helpers.nix | 10 +- lib/impl/modules/pipeline.nix | 9 +- tests/cilib_test.nix | 171 ++++++++++++++++++++++++++++++++++ tests/helpers_test.nix | 68 ++++++++++++++ tests/modules_test.nix | 81 ++++++++++++++++ tests/soonix_test.nix | 77 +++++++++++++++ tests/utils_test.nix | 32 +++++++ 7 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 tests/cilib_test.nix create mode 100644 tests/helpers_test.nix create mode 100644 tests/modules_test.nix create mode 100644 tests/soonix_test.nix create mode 100644 tests/utils_test.nix diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index ebeeb46..66a42cf 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -2,7 +2,7 @@ pkgs, lib, } @ args: let - inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType literalExpression; + inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType literalExpression pipe; in rec { prepend = key: arr: job: { ${key} = arr ++ (job.${key} or []); @@ -15,7 +15,13 @@ in rec { # json is also valid yaml and this removes dependency on jq and/or remarshal # (used in pkgs.formats.json and pkgs.formats.yaml respectively) - toYaml = name: value: builtins.toFile name (builtins.toJSON value); + toYaml = name: value: + pipe value [ + builtins.toJSON + builtins.unsafeDiscardOutputDependency + builtins.unsafeDiscardStringContext + (builtins.toFile name) + ]; toYamlPretty = (pkgs.formats.yaml {}).generate; customMapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); diff --git a/lib/impl/modules/pipeline.nix b/lib/impl/modules/pipeline.nix index 9d1e636..cfb6284 100644 --- a/lib/impl/modules/pipeline.nix +++ b/lib/impl/modules/pipeline.nix @@ -4,7 +4,7 @@ jobSubmodule, ... }: let - inherit (lib) mkOption types filterAttrs mergeAttrsList pipe mapAttrs; + inherit (lib) mkOption types filterAttrs mergeAttrsList mapAttrs; inherit (cilib.helpers) filterUnset mkUnsetOption toYaml toYamlPretty; pipelineConfigSubmodule = {rootConfig, ...}: { @@ -93,12 +93,7 @@ // mapAttrs (_name: value: value.finalConfig) config.jobs; packages = { - "gitlab-ci:pipeline:${name}" = pipe config.finalConfig [ - builtins.toJSON - builtins.unsafeDiscardOutputDependency - builtins.unsafeDiscardStringContext - (toYaml "gitlab-ci-config.json") - ]; + "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)); diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix new file mode 100644 index 0000000..d37da49 --- /dev/null +++ b/tests/cilib_test.nix @@ -0,0 +1,171 @@ +{ + pkgs, + cilib, + ntlib, + ... +}: { + suites."CI Lib" = { + pos = __curPos; + tests = let + inherit (cilib) mkJobDeps mkJobRun mkJobPatched mkPipeline; + deps = mkJobDeps { + key = "test"; + job = { + variables.TEST = "${pkgs.curl}"; + }; + nixConfig = { + enable = true; + deps = [pkgs.hello]; + }; + }; + in [ + { + name = "jobDeps"; + type = "script"; + script = + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${deps} "/nix/store" "should contain nix store path" + assert_file_contains ${deps} '-hello-.*/bin:$PATH' "should contain hello" + assert_file_contains ${deps} "export TEST=" "should export TEST" + assert_file_contains ${deps} "curl" "should contain curl" + ''; + } + { + name = "jobRun"; + type = "script"; + script = let + run = mkJobRun { + key = "test"; + job.script = ["hello"]; + jobDeps = deps; + }; + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${run}/bin/gitlab-ci-job:test "sandbox-helper" "should contain sandbox-helper" + assert_file_contains ${run}/bin/gitlab-ci-job:test "gitlab-ci-job-test-raw" "should contain job name" + assert_file_contains ${run.passthru.actualJobScript} "gitlab-ci-job-deps-test" "should contain job name" + assert_file_contains ${run.passthru.actualJobScript} "Running script..." "should contain 'Running script...'" + assert_file_contains ${run.passthru.actualJobScript} "hello" "should contain hello" + ''; + } + { + name = "jobPatched nix disabled"; + expected = {}; + actual = mkJobPatched { + key = "test"; + pipelineName = "test"; + job = {}; + nixConfig.enable = false; + }; + } + { + name = "jobPatched without runner cache"; + expected = { + after_script = ["finalize_nix_ci"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + }; + actual = mkJobPatched { + key = "test"; + pipelineName = "test"; + job = {}; + nixConfig = { + enable = true; + enableRunnerCache = false; + }; + }; + } + { + name = "jobPatched with runner cache"; + expected = { + after_script = ["finalize_nix_ci"]; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + cache = [ + { + key = "test"; + paths = [".nix-cache/"]; + } + ]; + variables."NIX_CI_CACHE_STRATEGY" = "runner"; + }; + actual = mkJobPatched { + key = "test"; + pipelineName = "test"; + job = {}; + nixConfig = { + enable = true; + enableRunnerCache = true; + runnerCacheKey = "test"; + }; + }; + } + { + name = "mkPipeline empty"; + expected = {}; + actual = + (mkPipeline { + name = "test"; + nixConfig = {}; + pipeline.jobs = {}; + }).finalConfig; + } + { + name = "mkPipeline empty packages"; + type = "script"; + script = let + pipeline = + ntlib.helpers.toJsonFile + (mkPipeline { + name = "test"; + nixConfig = {}; + pipeline.jobs = {}; + }).packages; + in + # sh + '' + set -euo pipefail + ${ntlib.helpers.path [pkgs.jq pkgs.gnugrep pkgs.coreutils]} + echo "two keys, one json one pretty" + jq 'keys | length == 2' "${pipeline}" | grep -q true + echo "key[0] is exactly 'gitlab-ci:pipeline:test'" + jq -r 'keys[0]' "${pipeline}" | grep -qx "gitlab-ci:pipeline:test" + echo "key[1] is exactly 'gitlab-ci:pipeline:test:pretty'" + jq -r 'keys[1]' "${pipeline}" | grep -qx "gitlab-ci:pipeline:test:pretty" + echo "value contains '/nix/store/'" + jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "/nix/store/" + echo "value contains 'gitlab-ci-test.yml'" + jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "gitlab-ci-test.yml" + echo "file only contains '{}'" + [[ "$(cat $(jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}"))" == "{}" ]] + ''; + } + { + name = "handle store paths in variables"; + expected = { + stages = ["test"]; + test = { + stage = "test"; + variables."TEST" = "${pkgs.hello}"; + }; + }; + actual = + (mkPipeline { + name = "test"; + nixConfig.enable = false; + pipeline = { + stages = ["test"]; + jobs.test = { + stage = "test"; + variables."TEST" = "${pkgs.hello}"; + }; + }; + }).finalConfig; + } + ]; + }; +} diff --git a/tests/helpers_test.nix b/tests/helpers_test.nix new file mode 100644 index 0000000..4d7e7d4 --- /dev/null +++ b/tests/helpers_test.nix @@ -0,0 +1,68 @@ +{ + pkgs, + cilib, + ... +}: { + suites."Helpers" = { + pos = __curPos; + tests = let + inherit (cilib) helpers; + in [ + { + name = "appendToAfterScript"; + expected = { + after_script = ["echo after_script" "finalize_nix_ci"]; + }; + actual = helpers.appendToAfterScript ["finalize_nix_ci"] { + after_script = ["echo after_script"]; + }; + } + { + name = "prependToBeforeScript"; + expected = { + before_script = ["setup_nix_ci" "echo before_script"]; + }; + actual = helpers.prependToBeforeScript ["setup_nix_ci"] { + before_script = ["echo before_script"]; + }; + } + { + name = "toYaml"; + expected = ''{"hello":"world"}''; + actual = builtins.readFile (helpers.toYaml "test" {hello = "world";}); + } + { + name = "filterAttrsRec"; + expected = {world = "world";}; + actual = helpers.filterAttrsRec (_n: v: v != null) { + hello = null; + world = "world"; + }; + } + { + name = "filterJobVariables with store paths"; + expected = { + HELLO = "${pkgs.hello}"; + MULTIPLE = "${pkgs.hello}:${pkgs.hello}"; + }; + actual = helpers.filterJobVariables true { + variables = { + HELLO = "${pkgs.hello}"; + WORLD = "world"; + MULTIPLE = "${pkgs.hello}:${pkgs.hello}"; + }; + }; + } + { + name = "filterJobVariables without store paths"; + expected = {WORLD = "world";}; + actual = helpers.filterJobVariables false { + variables = { + HELLO = "${pkgs.hello}"; + WORLD = "world"; + }; + }; + } + ]; + }; +} diff --git a/tests/modules_test.nix b/tests/modules_test.nix new file mode 100644 index 0000000..0b467f4 --- /dev/null +++ b/tests/modules_test.nix @@ -0,0 +1,81 @@ +{ + pkgs, + cilib, + ntlib, + ... +}: { + suites."Modules" = { + pos = __curPos; + tests = let + simplePipeline = cilib.mkCI { + pipelines."test" = { + stages = ["test"]; + jobs."test" = { + stage = "test"; + script = ["echo hello world"]; + }; + }; + }; + in [ + { + name = "empty pipelines"; + expected = {}; + actual = + (cilib.mkCI {}).pipelines; + } + { + name = "empty packages"; + expected = {}; + actual = + (cilib.mkCI {}).packages; + } + { + name = "simple pipeline"; + expected = { + stages = [".pre" "test" ".post"]; + "test" = { + stage = "test"; + before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; + script = ["echo hello world"]; + after_script = ["finalize_nix_ci"]; + }; + }; + actual = simplePipeline.pipelines."test".finalConfig; + } + { + name = "simple pipeline yaml"; + type = "script"; + script = let + package = simplePipeline.packages."gitlab-ci:pipeline:test"; + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${package} 'gitlab-ci:pipeline:test:job-deps:test' + assert_file_contains ${package} 'finalize_nix_ci' + assert_file_contains ${package} 'echo hello world' + ''; + } + { + name = "dont fail on store paths"; + type = "script"; + script = let + package = + (cilib.mkCI { + pipelines."test" = { + variables.EXAMPLE = "${pkgs.hello}"; + }; + }).packages."gitlab-ci:pipeline:test"; + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${package} '[".pre",".post"]' + assert_file_contains ${package} '"EXAMPLE":"/nix/store/.*-hello-.*"' + ''; + } + ]; + }; +} diff --git a/tests/soonix_test.nix b/tests/soonix_test.nix new file mode 100644 index 0000000..c76edb4 --- /dev/null +++ b/tests/soonix_test.nix @@ -0,0 +1,77 @@ +{ + lib, + cilib, + ... +}: let + inherit (lib) trimWith; +in { + suites."Soonix" = { + pos = __curPos; + tests = let + version = trimWith { + start = true; + end = true; + } (builtins.readFile ../lib/VERSION); + in [ + { + name = "default soonix config"; + expected = { + data.include = [ + { + component = "gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@${version}"; + inputs.version = version; + } + ]; + generator = "nix"; + opts.format = "yaml"; + hook = { + mode = "copy"; + gitignore = false; + }; + output = ".gitlab-ci.yml"; + }; + actual = + (cilib.mkCI { + config.soonix = {}; + }).soonix; + } + { + name = "custom soonix config"; + expected = { + data = { + include = [ + { + component = "gitlab.com/example/nix-gitlab-ci/nix-gitlab-ci@abc"; + inputs = { + version = "abc"; + hello = "world"; + }; + } + ]; + "example".script = ["hello"]; + }; + generator = "nix"; + opts.format = "yaml"; + hook = { + mode = "copy"; + gitignore = false; + }; + output = ".gitlab-ci.yml"; + }; + actual = + (cilib.mkCI { + config.soonix = { + componentVersion = "abc"; + componentUrl = "gitlab.com/example/nix-gitlab-ci/nix-gitlab-ci"; + componentInputs = { + hello = "world"; + }; + extraData = { + "example".script = ["hello"]; + }; + }; + }).soonix; + } + ]; + }; +} diff --git a/tests/utils_test.nix b/tests/utils_test.nix new file mode 100644 index 0000000..2e319b1 --- /dev/null +++ b/tests/utils_test.nix @@ -0,0 +1,32 @@ +{ + pkgs, + ntlib, + cilib, + ... +}: { + suites."Utils" = { + pos = __curPos; + tests = [ + { + name = "commitAndPushFiles"; + type = "script"; + script = let + inherit (cilib) utils; + job = ntlib.helpers.toJsonFile ( + utils.commitAndPushFiles { + message = "hello world"; + files = ["a.md" "b.txt"]; + } {} + ); + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${job} 'git commit -m \\"hello world\\"' + assert_file_contains ${job} 'git add a.md b.txt' + ''; + } + ]; + }; +} From c4a439b839e5408504fb2f7cab1ecbb18b54309e Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Sep 2025 13:26:06 +0200 Subject: [PATCH 073/102] chore(repo): update nixtest --- nix/repo/flake.lock | 6 +++--- tests/flake_parts_test.nix | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index 0463608..b5de491 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -37,11 +37,11 @@ "nixtest-lib": { "locked": { "dir": "lib", - "lastModified": 1753957623, - "narHash": "sha256-kdImwKx57N0QL8HPUUb5ADwXFgSjaNOk39b/eKlzyTo=", + "lastModified": 1756812148, + "narHash": "sha256-0g8KNk4zoLApA51PBHOWqPLRYpprjrQuSzNCjfBQgu8=", "owner": "TECHNOFAB", "repo": "nixtest", - "rev": "22b43c9fe83be73c3f0648bbb54bc3c1cf7f96df", + "rev": "5741109cc9ec2b6d41b56abd3f5bc51ed7a9a228", "type": "gitlab" }, "original": { diff --git a/tests/flake_parts_test.nix b/tests/flake_parts_test.nix index f192420..0b11ef2 100644 --- a/tests/flake_parts_test.nix +++ b/tests/flake_parts_test.nix @@ -16,9 +16,6 @@ ${ntlib.helpers.path (with pkgs; [coreutils nix gnused gnugrep jq])} repo_path=${../.} - dir=$(mktemp -d) - trap "rm -rf $dir" EXIT - cd $dir cp ${./fixtures/flake_parts}/* . # import from the absolute path above, is easier than trying to figure out the repo path etc. sed -i -e "s|@repo_path@|$repo_path|" flake.nix From 6cd05e503a1946301ff3d9e25ee87fb52e056c0c Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Sep 2025 10:18:12 +0200 Subject: [PATCH 074/102] fix(ci): only specify component version once --- .gitlab-ci.yml | 2 +- nix/repo/ci.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c6bc9f2..3dc24e7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,7 +45,7 @@ deploy:image: \ localhost/nix-ci docker://${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_TAG}\nfi\n" stage: build-images include: -- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA@$CI_COMMIT_SHORT_SHA +- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHORT_SHA inputs: version: $CI_COMMIT_SHORT_SHA stages: diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 9362a05..38a650d 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -3,7 +3,7 @@ in cilib.mkCI { config.soonix = { - componentUrl = "$CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA"; + componentUrl = "$CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci"; componentVersion = "$CI_COMMIT_SHORT_SHA"; # bootstrapping still needs to be done in the gitlab-ci.yml directly, # the child pipeline can then use the built images to test them From 4d824900d4f2f5ac59df444b33f1c13200f35e3a Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Sep 2025 10:46:47 +0200 Subject: [PATCH 075/102] fix(module): default image to $NIX_CI_IMAGE --- lib/impl/modules/job.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index d8b91c0..b8d9437 100644 --- a/lib/impl/modules/job.nix +++ b/lib/impl/modules/job.nix @@ -59,7 +59,7 @@ in rec { ''; type = types.str; }; - image = mkUnsetOption { + image = mkOption { description = '' Container/OCI image to use for this job. @@ -67,6 +67,7 @@ in rec { Setting this will mess with Nix-GitLab-CI, so be careful and only use for non-nix jobs. ''; type = types.str; + default = "$NIX_CI_IMAGE"; }; variables = mkUnsetOption { type = types.attrsOf types.str; From cf04bf53572bc36588348a5ec368d72cea93f05b Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Sep 2025 14:14:25 +0200 Subject: [PATCH 076/102] fix(sandbox_helper): fix TMPDIR being initialized too late and thus removing /tmp --- lib/impl/sandbox_helper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/impl/sandbox_helper.sh b/lib/impl/sandbox_helper.sh index 4ea5ec5..294fc6b 100644 --- a/lib/impl/sandbox_helper.sh +++ b/lib/impl/sandbox_helper.sh @@ -35,6 +35,7 @@ done if [ "$NO_SANDBOX" = false ]; then echo "Running with simple sandboxing" + TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") if [ "$KEEP_TMP" = false ]; then trap "rm -rf '$TMPDIR'" EXIT else @@ -49,7 +50,6 @@ if [ "$NO_SANDBOX" = false ]; then git diff --staged > "$DIRTY_PATCH" trap "rm -f '$DIRTY_PATCH'" EXIT fi - TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") git clone . $TMPDIR pushd $TMPDIR >/dev/null if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then From a076f0048a640ed35c27a97479e93cd47388c83f Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Sep 2025 14:29:56 +0200 Subject: [PATCH 077/102] chore(CI): modify cache_files and add ssl ca certs to test job --- .gitlab-ci.yml | 3 +++ nix/repo/ci.nix | 7 ++++++- tests/modules_test.nix | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3dc24e7..bff742c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,6 +47,9 @@ deploy:image: include: - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHORT_SHA inputs: + cache_files: + - flake.* + - nix/repo/ci.nix version: $CI_COMMIT_SHORT_SHA stages: - build-images diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 38a650d..5e14188 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -1,10 +1,11 @@ {inputs, ...}: let - inherit (inputs) cilib; + inherit (inputs) pkgs cilib; in cilib.mkCI { config.soonix = { componentUrl = "$CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci"; componentVersion = "$CI_COMMIT_SHORT_SHA"; + componentInputs.cache_files = ["flake.*" "nix/repo/ci.nix"]; # bootstrapping still needs to be done in the gitlab-ci.yml directly, # the child pipeline can then use the built images to test them extraData = { @@ -63,6 +64,10 @@ in script = [ "nix run .#tests -- --junit=junit.xml" ]; + variables = { + "SSL_CERT_FILE" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + "NIX_SSL_CERT_FILE" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + }; allow_failure = true; artifacts = { when = "always"; diff --git a/tests/modules_test.nix b/tests/modules_test.nix index 0b467f4..e134293 100644 --- a/tests/modules_test.nix +++ b/tests/modules_test.nix @@ -34,6 +34,7 @@ expected = { stages = [".pre" "test" ".post"]; "test" = { + image = "$NIX_CI_IMAGE"; stage = "test"; before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; script = ["echo hello world"]; From 31f0e4ea13c182d74121419e9f85312d9242eb46 Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Sep 2025 15:17:51 +0200 Subject: [PATCH 078/102] fix(jobPatched): fix variables not being unset if all contain store paths the goal was to remove "variables" when it's empty, but this resulted in the original value being used if all of the variables contained store paths. This is not supposed to happen and thus fixed with this. --- lib/impl/helpers.nix | 22 ++++++++++++++++++---- lib/impl/jobPatched.nix | 38 +++++++++++++------------------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 66a42cf..4b08c7f 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -2,7 +2,21 @@ pkgs, lib, } @ args: let - inherit (lib) types isAttrs filterAttrs mapAttrs mkOption mkOptionType isType literalExpression pipe; + inherit + (lib) + types + isAttrs + filterAttrs + mapAttrs + mkOption + mkOptionType + isType + literalExpression + pipe + hasInfix + concatMapAttrs + optionalAttrs + ; in rec { prepend = key: arr: job: { ${key} = arr ++ (job.${key} or []); @@ -33,10 +47,10 @@ in rec { # filter job's variables to either only those containing store paths # or those that do not - filterJobVariables = nix: job: - lib.concatMapAttrs ( + filterJobVariables = shouldContain: job: + concatMapAttrs ( name: value: - lib.optionalAttrs ((lib.hasInfix "/nix/store/" value) == nix) { + optionalAttrs ((hasInfix "/nix/store/" value) == shouldContain) { ${name} = value; } ) diff --git a/lib/impl/jobPatched.nix b/lib/impl/jobPatched.nix index 4bf0e00..9997caa 100644 --- a/lib/impl/jobPatched.nix +++ b/lib/impl/jobPatched.nix @@ -16,28 +16,16 @@ in (prependToBeforeScript ["source setup_nix_ci \"gitlab-ci:pipeline:${pipelineName}:job-deps:${key}\""] job) // (appendToAfterScript ["finalize_nix_ci"] job) )) - // optionalAttrs nixConfig.enable ( - (let - variables = - (filterJobVariables false job) - // optionalAttrs nixConfig.enableRunnerCache { - NIX_CI_CACHE_STRATEGY = "runner"; - }; - in - # filter empty variables - optionalAttrs (variables != {}) { - inherit variables; - }) - // (let - cache = - (toList (job.cache or [])) - ++ (optional nixConfig.enableRunnerCache { - key = nixConfig.runnerCacheKey; - paths = [".nix-cache/"]; - }); - in - # filter empty cache - optionalAttrs (cache != []) { - inherit cache; - }) - ) + // optionalAttrs nixConfig.enable { + variables = + (filterJobVariables false job) + // optionalAttrs nixConfig.enableRunnerCache { + NIX_CI_CACHE_STRATEGY = "runner"; + }; + cache = + (toList (job.cache or [])) + ++ (optional nixConfig.enableRunnerCache { + key = nixConfig.runnerCacheKey; + paths = [".nix-cache/"]; + }); + } From 5e2ae29660ef02abd65bd54bf295f7af3bebcb44 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 4 Sep 2025 08:34:25 +0200 Subject: [PATCH 079/102] chore: better solution for handling store paths in variables --- lib/impl/jobPatched.nix | 40 ++++++++++++++++++++++++++-------------- tests/cilib_test.nix | 5 +---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/impl/jobPatched.nix b/lib/impl/jobPatched.nix index 9997caa..c7134fb 100644 --- a/lib/impl/jobPatched.nix +++ b/lib/impl/jobPatched.nix @@ -11,21 +11,33 @@ in pipelineName, nixConfig, }: - job + (builtins.removeAttrs job ["variables" "cache"]) // (optionalAttrs nixConfig.enable ( (prependToBeforeScript ["source setup_nix_ci \"gitlab-ci:pipeline:${pipelineName}:job-deps:${key}\""] job) // (appendToAfterScript ["finalize_nix_ci"] job) )) - // optionalAttrs nixConfig.enable { - variables = - (filterJobVariables false job) - // optionalAttrs nixConfig.enableRunnerCache { - NIX_CI_CACHE_STRATEGY = "runner"; - }; - cache = - (toList (job.cache or [])) - ++ (optional nixConfig.enableRunnerCache { - key = nixConfig.runnerCacheKey; - paths = [".nix-cache/"]; - }); - } + // optionalAttrs nixConfig.enable ( + (let + variables = + (filterJobVariables false job) + // optionalAttrs nixConfig.enableRunnerCache { + NIX_CI_CACHE_STRATEGY = "runner"; + }; + in + # filter empty variables + optionalAttrs (variables != {}) { + inherit variables; + }) + // (let + cache = + (toList (job.cache or [])) + ++ (optional nixConfig.enableRunnerCache { + key = nixConfig.runnerCacheKey; + paths = [".nix-cache/"]; + }); + in + # filter empty cache + optionalAttrs (cache != []) { + inherit cache; + }) + ) diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix index d37da49..78e99a3 100644 --- a/tests/cilib_test.nix +++ b/tests/cilib_test.nix @@ -148,10 +148,7 @@ name = "handle store paths in variables"; expected = { stages = ["test"]; - test = { - stage = "test"; - variables."TEST" = "${pkgs.hello}"; - }; + test.stage = "test"; }; actual = (mkPipeline { From 2a0a3f5881526a7272796baec0a436fe0c8b0ddc Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 4 Sep 2025 08:39:37 +0200 Subject: [PATCH 080/102] chore(ci): try setting ssl env vars in the test directly (cuz pure mode) --- nix/repo/ci.nix | 4 ---- tests/flake_parts_test.nix | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 5e14188..747c9d8 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -64,10 +64,6 @@ in script = [ "nix run .#tests -- --junit=junit.xml" ]; - variables = { - "SSL_CERT_FILE" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - "NIX_SSL_CERT_FILE" = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - }; allow_failure = true; artifacts = { when = "always"; diff --git a/tests/flake_parts_test.nix b/tests/flake_parts_test.nix index 0b11ef2..e998f45 100644 --- a/tests/flake_parts_test.nix +++ b/tests/flake_parts_test.nix @@ -14,6 +14,8 @@ '' ${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}/* . From 00cf5b83c6c46698fba12a54b9cc15c6d4e5a4dd Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 4 Sep 2025 10:29:56 +0200 Subject: [PATCH 081/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index 0786c6b..18b1307 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.0-alpha.1 +3.0.0-alpha.2 From 428afaf603b91a83833d93561e680f054d087d18 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 5 Sep 2025 09:02:42 +0200 Subject: [PATCH 082/102] chore(repo): update nixmkdocs --- nix/repo/flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index b5de491..0627f8b 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -20,11 +20,11 @@ "nixmkdocs-lib": { "locked": { "dir": "lib", - "lastModified": 1755785622, - "narHash": "sha256-xBb9PCkszmrWSEqUiPC7oBJABm1thF572S5QHtloZ+M=", + "lastModified": 1757055638, + "narHash": "sha256-KHYSkEreFe4meXzSdEbknC/HwaQSNClQkc8vzHlAsMM=", "owner": "TECHNOFAB", "repo": "nixmkdocs", - "rev": "61da605a9bff12f66c4b743f43aea59ca200f533", + "rev": "7840a5febdbeaf2da90babf6c94b3d0929d2bf74", "type": "gitlab" }, "original": { From e752f71dd17c89836220237033fcf6dff2bbf01c Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 25 Sep 2025 13:18:06 +0200 Subject: [PATCH 083/102] docs: use svg logo, add style.css and fix sitemap --- docs/images/favicon.png | Bin 1995 -> 0 bytes docs/images/logo.png | Bin 40993 -> 0 bytes docs/images/logo.svg | 1 + docs/style.css | 15 +++++++++++++++ nix/repo/ci.nix | 2 +- nix/repo/docs.nix | 6 ++++-- 6 files changed, 21 insertions(+), 3 deletions(-) delete mode 100644 docs/images/favicon.png delete mode 100644 docs/images/logo.png create mode 100755 docs/images/logo.svg create mode 100644 docs/style.css diff --git a/docs/images/favicon.png b/docs/images/favicon.png deleted file mode 100644 index 135b03026d6a5ffb7cbeec80c97224e5b6eb776d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1995 zcmV;+2Q>JJP)Ilb@!-~pgw+p^fd`_57-dFY%EX0E7d_G7}<8 z{n2aqBQ$P)exR!E0QLd}$?1jt8xb1_N?<%jn!WaNwNK(1q7lBs^1kPvDpBkHxAB)z>XxxIyZ|R}wo{HJ=6Dn{QP$^KDk_0i-n|xe#zGce`^2N_Ptw zDEXr1=DW_~F|+p=Ci*yHh#-gvi1COZqY!b4$Pi#E$TNu4h`6^C5qiZ&jdwT$=As_6 zp(v|?4?(}kOzKkXFF;3A6Q-I!0NE&R1t=hx=O$kKqR-&6cb0>(BJJT-(IV_~8%7XI z?yFBu|FPH5s1u-Ft>t^CWj(!CwC+VNWX2%Vhj9s@l!Lmhp3vrv0RDQ zt){tQaM?SFz+^CIqPn@kyfv<0t9!QMeB~yu1ywhR)=dW{0naw{p}YjR#-|qs-9ZfM z&s5EBKFU=v{{T#}ghmgo{$`)5S&dE3`rC2xKW86DwGgGYbxqYCV7#?%immwfwBqtEUqHey!46|F!zx?@zlaR@^9f%hE^4G)@= z^2Sq(K|k`Q8}yIbU-Wr$df`rBw8gJb=#{f9_RMW#@GsdD{?r)%xA8Hr`dEF!&NoL+ zzg3WDynmt?be4}%rY5HsZU9Ev_!R<`-@444da{?piXl6VEsc+H+-lwPD-%nBWFPj# z#PZ`MK!uI>;i7Hb?n3J9-W$Lj%ylRy z7w#B8^U61srL|Wi$a9^#jROT2swl0g#lwEEMG%7`LOE1k!{IAeN$e0#haksIT7z2X z^^o=v-G1D9$!xfCAcRvjwY>FpB~P>syMfDquy#sVYm)#6PUTXPs(Aq9r-E3; zUO=QL%3L7R12D7^V4j~dKoo!x0O4&-LR!M5(*~C>)dR4+M;jiE2)$+Pa)u}o_bZM- zwaJ-Ls`|BfZz`{5M5w+IgQ{7;aFq6-#*G_*u0RW*tU=&9pr=0-k74zxJGb4D@d(kAlMmb!s<`2n2`e zZ8Iw(Qfb}MY-+k-#~UO50c77%yY2L7q(OyW_u?jKG7y>%eP1qlMo|A}T9U zicnpZIkuZ`d%h3Ym~eUS#NxUku|)A_qOH$$-<`VxUD$mtEOBsJ-|b_pL~GxO$|DME z{Z5is#zbFxdE2q+ihkC}^e+G>FneazkRff022I@9>CKOTB|i7=bs%F=t1TPLTLx|h zhBo;*1`F1v#?;=`(+5D@WLIC{pB6w5T?D4(j>tMI=zCVja~gPV*43N?qQ()RyA8}w zfKToHPIhX{XLtQJ6F1o}Xr2{w9{SKQ z>f_d=Jf!~UHSWNGu2qsc+4%8!>r?aW>u0!%#sWDueI-Dj?9`Z==CWX{4IDroVrJo0;=pnpU}rrFnLr}kE0 zo|V}EpCasS)(6bE$*xE%8!PkHrylZ|>k9M)>a9_pw?6gXzI)7Y6+H}usd94d3o&K) d-jDAQ@IM<&9OUDTXUhNp002ovPDHLkV1kV3&P@OS diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 2a70bf66f5680e7b7c454e0a850d5ce731562603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40993 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 \ No newline at end of file diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..b2ae4ff --- /dev/null +++ b/docs/style.css @@ -0,0 +1,15 @@ +.md-header__button.md-logo { + margin: 0; + padding-top: .2rem; + padding-bottom: .2rem; +} + +[dir="ltr"] .md-header__title { + margin-left: 0; +} + +.md-header__button.md-logo img, +.md-header__button.md-logo svg { + height: 2rem; +} + diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 747c9d8..a76c0d8 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -1,5 +1,5 @@ {inputs, ...}: let - inherit (inputs) pkgs cilib; + inherit (inputs) cilib; in cilib.mkCI { config.soonix = { diff --git a/nix/repo/docs.nix b/nix/repo/docs.nix index bb05f74..052b177 100644 --- a/nix/repo/docs.nix +++ b/nix/repo/docs.nix @@ -38,12 +38,14 @@ in }; config = { site_name = "Nix-GitLab-CI"; + site_url = "https://nix-gitlab-ci.projects.tf"; repo_name = "TECHNOFAB/nix-gitlab-ci"; repo_url = "https://gitlab.com/TECHNOFAB/nix-gitlab-ci"; + extra_css = ["style.css"]; theme = { - logo = "images/logo.png"; + logo = "images/logo.svg"; icon.repo = "simple/gitlab"; - favicon = "images/favicon.png"; + favicon = "images/logo.svg"; }; nav = [ {"Introduction" = "index.md";} From 0cca02f4425d0e6104cd2ce000316a61141e4bde Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 25 Sep 2025 14:49:46 +0200 Subject: [PATCH 084/102] feat: implement all (?) ci yaml keywords --- lib/impl/helpers.nix | 10 +- lib/impl/modules/job.nix | 528 ++++++++++++++++++++++++++++++++-- lib/impl/modules/pipeline.nix | 132 ++++++++- 3 files changed, 649 insertions(+), 21 deletions(-) diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 4b08c7f..48a79fc 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -84,7 +84,7 @@ in rec { unsetOr = typ: (types.either unsetType typ) // { - inherit (typ) description; + inherit (typ) description getSubOptions; }; mkUnsetOption = opts: mkOption (opts @@ -93,6 +93,14 @@ in rec { default = opts.default or unset; defaultText = literalExpression "unset"; }); + eitherWithSubOptions = typ_one: typ_two: + (types.either typ_one typ_two) + // { + getSubOptions = + if (typ_one.getSubOptions "test" != {}) + then typ_one.getSubOptions + else typ_two.getSubOptions; + }; filterUnset = value: if builtins.isAttrs value && !builtins.hasAttr "_type" value diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index b8d9437..5f7d1ee 100644 --- a/lib/impl/modules/job.nix +++ b/lib/impl/modules/job.nix @@ -4,7 +4,7 @@ ... }: let inherit (lib) mkOption types filterAttrs; - inherit (cilib.helpers) filterUnset mkUnsetOption; + inherit (cilib.helpers) filterUnset mkUnsetOption eitherWithSubOptions; in rec { jobConfigSubmodule = {pipelineConfig, ...}: { options = { @@ -53,43 +53,539 @@ in rec { # GITLAB OPTIONS # gitlabOptions = { - stage = mkOption { + # see https://docs.gitlab.com/ci/yaml/ + after_script = mkUnsetOption { + type = types.listOf types.str; description = '' - Pipeline stage to run this job in. + Override a set of commands that are executed after job. + + [Docs](https://docs.gitlab.com/ci/yaml/#after_script) ''; + }; + allow_failure = mkUnsetOption { + type = eitherWithSubOptions types.bool (types.submodule { + options = { + exit_codes = mkUnsetOption { + type = types.either types.number (types.listOf types.number); + description = '' + Use `allow_failure.exit_codes` to control when a job should be allowed to fail. + The job is `allow_failure = true` for any of the listed exit codes, and `allow_failure = false` for any other exit code. + ''; + }; + }; + }); + description = '' + Allow job to fail. A failed job does not cause the pipeline to fail. + + [Docs](https://docs.gitlab.com/ci/yaml/#allow_failure) + ''; + }; + artifacts = mkUnsetOption { + # TODO: can be used in pipeline.default aswell + type = types.submodule { + options = { + paths = mkUnsetOption { + type = types.listOf types.str; + description = '' + Paths are relative to the project directory (`$CI_PROJECT_DIR`) and can’t directly link outside it. + ''; + }; + excludes = mkUnsetOption { + type = types.listOf types.str; + description = '' + Use `exclude` to prevent files from being added to an artifacts archive. + ''; + }; + expire_in = mkUnsetOption { + type = types.str; + description = '' + Use `expire_in` to specify how long [job artifacts](https://docs.gitlab.com/ci/jobs/job_artifacts/) are stored before they expire and are deleted. + ''; + }; + expose_as = mkUnsetOption { + type = types.str; + description = '' + Use the `expose_as` keyword to [expose artifacts in the merge request UI](https://docs.gitlab.com/ci/jobs/job_artifacts/#link-to-job-artifacts-in-the-merge-request-ui). + ''; + }; + name = mkUnsetOption { + type = types.str; + description = '' + Use the `name` keyword to define the name of the created artifacts archive. You can specify a unique name for every archive. + ''; + }; + public = mkUnsetOption { + type = types.bool; + description = '' + Use `public` to determine whether the job artifacts should be publicly available. + ''; + }; + access = mkUnsetOption { + type = types.enum [ + "all" + "developer" + "maintainer" + "none" + ]; + description = '' + Use `access` to determine who can access the job artifacts from the GitLab UI or API. + This option does not prevent you from forwarding artifacts to downstream pipelines. + ''; + }; + reports = mkUnsetOption { + type = types.attrs; + description = '' + Use `reports` to collect artifacts generated by included templates in jobs. + ''; + }; + untracked = mkUnsetOption { + type = types.bool; + description = '' + Use `untracked` to add all Git untracked files as artifacts (along with the paths defined in `paths`). + `untracked` ignores configuration in the repository’s .gitignore, so matching artifacts in .gitignore are included. + ''; + }; + when = mkUnsetOption { + type = types.enum [ + "on_success" + "on_failure" + "always" + ]; + description = '' + Use `when` to upload artifacts on job failure or despite the failure. + ''; + }; + }; + }; + description = '' + List of files and directories to attach to a job on success. + + [Docs](https://docs.gitlab.com/ci/yaml/#artifacts) + ''; + }; + before_script = mkUnsetOption { + type = types.listOf types.str; + description = '' + Override a set of commands that are executed before job. + + [Docs](https://docs.gitlab.com/ci/yaml/#before_script) + ''; + }; + cache = mkUnsetOption { + # could be more granular + type = types.either (types.listOf types.attrs) types.attrs; + description = '' + List of files that should be cached between subsequent runs. + + [Docs](https://docs.gitlab.com/ci/yaml/#cache) + ''; + }; + coverage = mkUnsetOption { type = types.str; + description = '' + Code coverage settings for a given job. + + [Docs](https://docs.gitlab.com/ci/yaml/#coverage) + ''; + }; + dast_configuration = mkUnsetOption { + type = types.attrs; + description = '' + Use configuration from DAST profiles on a job level. + + [Docs](https://docs.gitlab.com/ci/yaml/#dast_configuration) + ''; + }; + dependencies = mkUnsetOption { + type = types.listOf types.str; + description = '' + Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. + + [Docs](https://docs.gitlab.com/ci/yaml/#dependencies) + ''; + }; + environment = mkUnsetOption { + type = eitherWithSubOptions types.str (types.submodule { + options = { + name = mkUnsetOption { + type = types.str; + example = "production"; + description = '' + Set a name for an environment. + ''; + }; + url = mkUnsetOption { + type = types.str; + example = "https://prod.example.com"; + description = '' + Set a URL for an environment. + ''; + }; + on_stop = mkUnsetOption { + type = types.str; + example = "down"; + description = '' + Closing (stopping) environments can be achieved with the `on_stop` keyword defined under `environment`. + It declares a different job that runs to close the environment. + ''; + }; + action = mkUnsetOption { + type = types.enum ["start" "prepare" "stop" "verify" "access"]; + description = '' + Use the `action` keyword to specify how the job interacts with the environment. + ''; + }; + auto_stop_in = mkUnsetOption { + type = types.str; + description = '' + The `auto_stop_in` keyword specifies the lifetime of the environment. + When an environment expires, GitLab automatically stops it. + ''; + }; + kubernetes = mkUnsetOption { + type = types.attrs; # no we don't go that deep :D + description = '' + Use the `kubernetes` keyword to configure the + [dashboard for Kubernetes](https://docs.gitlab.com/ci/environments/kubernetes_dashboard/) and + [GitLab-managed Kubernetes resources](https://docs.gitlab.com/user/clusters/agent/managed_kubernetes_resources/) + for an environment. + ''; + }; + deployment_tier = mkUnsetOption { + type = types.enum [ + "production" + "staging" + "testing" + "development" + "other" + ]; + description = '' + Use the `deployment_tier` keyword to specify the tier of the deployment environment. + ''; + }; + }; + }); + description = '' + Name of an environment to which the job deploys. + See the implementation for nested options, or check out the docs: + + [Docs](https://docs.gitlab.com/ci/yaml/#environment) + ''; + example = { + name = "review/$CI_COMMIT_REF_SLUG"; + url = "https://$CI_COMMIT_REF_SLUG.review.example.com"; + action = "stop"; + auto_stop_in = "1 day"; + deployment_tier = "staging"; + }; + }; + extends = mkUnsetOption { + type = types.either types.str (types.listOf types.str); + description = '' + Configuration entries that this job inherits from. + + [Docs](https://docs.gitlab.com/ci/yaml/#extends) + ''; + }; + hooks = mkUnsetOption { + type = types.attrs; + description = '' + Use `hooks` to specify lists of commands to execute on the runner at certain stages of job execution, + like before retrieving the Git repository. + + [Docs](https://docs.gitlab.com/ci/yaml/#hooks) + ''; + }; + identity = mkUnsetOption { + type = types.str; + description = '' + Authenticate with third party services using identity federation. + + [Docs](https://docs.gitlab.com/ci/yaml/#identity) + ''; + }; + id_tokens = mkUnsetOption { + type = types.attrs; + description = '' + Use `id_tokens` to create [ID tokens](https://docs.gitlab.com/ci/secrets/id_token_authentication/) to authenticate with third party services + + [Docs](https://docs.gitlab.com/ci/yaml/#id_tokens) + ''; + example = { + ID_TOKEN_1.aud = "https://vault.example.com"; + ID_TOKEN_2.aud = [ + "https://gcp.com" + "https://aws.com" + ]; + SIGSTORE_ID_TOKEN.aud = "sigstore"; + }; }; image = mkOption { + # could be more granular + type = types.either types.str types.attrs; description = '' Container/OCI image to use for this job. !!! warning Setting this will mess with Nix-GitLab-CI, so be careful and only use for non-nix jobs. ''; - type = types.str; default = "$NIX_CI_IMAGE"; + example = { + name = "super/sql:experimental"; + entrypoint = [""]; + pull_policy = "if-not-present"; + docker = { + platform = "arm64/v8"; + user = "dave"; + }; + kubernetes.user = "1001"; + }; }; - variables = mkUnsetOption { - type = types.attrsOf types.str; + "inherit" = mkUnsetOption { + type = types.submodule { + options = { + default = mkUnsetOption { + type = types.either types.bool (types.listOf types.str); + description = '' + Use `inherit.default` to control the inheritance of [default keywords](https://docs.gitlab.com/ci/yaml/#default). + ''; + }; + variables = mkUnsetOption { + type = types.either types.bool (types.listOf types.str); + description = '' + Use `inherit.variables` to control the inheritance of [default variables](https://docs.gitlab.com/ci/yaml/#default-variables). + ''; + }; + }; + }; + description = '' + Select which global defaults all jobs inherit. + + [Docs](https://docs.gitlab.com/ci/yaml/#inherit) + ''; }; - before_script = mkUnsetOption { - type = types.listOf types.str; + interruptible = mkUnsetOption { + type = types.bool; + description = '' + Defines if a job can be canceled when made redundant by a newer run. + + [Docs](https://docs.gitlab.com/ci/yaml/#interruptible) + ''; + }; + manual_confirmation = mkUnsetOption { + type = types.str; + description = '' + Define a custom confirmation message for a manual job. + + [Docs](https://docs.gitlab.com/ci/yaml/#manual_confirmation) + ''; + }; + needs = mkUnsetOption { + # could be done more granular + type = types.listOf (types.either types.str types.attrs); + description = '' + Execute jobs earlier than the stage ordering. + + [Docs](https://docs.gitlab.com/ci/yaml/#needs) + ''; + }; + pages = mkUnsetOption { + type = eitherWithSubOptions types.bool (types.submodule { + options = { + publish = mkUnsetOption { + type = types.str; + description = '' + Use `pages.publish` to configure the content directory of a [`pages` job](https://docs.gitlab.com/ci/yaml/#pages). + ''; + }; + path_prefix = mkUnsetOption { + type = types.str; + description = '' + Use `pages.path_prefix` to configure a path prefix for [parallel deployments](https://docs.gitlab.com/user/project/pages/#parallel-deployments) of GitLab Pages. + ''; + }; + expire_in = mkUnsetOption { + type = types.str; + description = '' + Use `expire_in` to specify how long a deployment should be available before it expires. + After the deployment is expired, it’s deactivated by a cron job running every 10 minutes. + ''; + }; + }; + }); + description = '' + Upload the result of a job to use with GitLab Pages. + + [Docs](https://docs.gitlab.com/ci/yaml/#pages) + ''; + }; + parallel = mkUnsetOption { + type = eitherWithSubOptions types.number (types.listOf (types.submodule { + options = { + matrix = mkUnsetOption { + type = types.attrs; + description = '' + Use `parallel.matrix` to run a job multiple times in parallel in a single pipeline, but with different variable values for each instance of the job. + ''; + }; + }; + })); + description = '' + How many instances of a job should be run in parallel. + + [Docs](https://docs.gitlab.com/ci/yaml/#parallel) + ''; + example = { + matrix = [ + { + PROVIDER = "aws"; + STACK = [ + "monitoring" + "app1" + "app2" + ]; + } + { + PROVIDER = "ovh"; + STACK = ["monitoring" "backup" "app"]; + } + { + PROVIDER = ["gcp" "vultr"]; + STACK = ["data" "processing"]; + } + ]; + }; + }; + release = mkUnsetOption { + # could be more granular + type = types.attrs; + description = '' + Instructs the runner to generate a [release](https://docs.gitlab.com/user/project/releases/) object. + + [Docs](https://docs.gitlab.com/ci/yaml/#release) + ''; + }; + resource_group = mkUnsetOption { + type = types.str; + description = '' + Limit job concurrency. + + [Docs](https://docs.gitlab.com/ci/yaml/#resource_group) + ''; + }; + retry = mkUnsetOption { + type = eitherWithSubOptions (types.ints.between 0 2) (types.submodule { + options = { + exit_codes = mkUnsetOption { + type = types.either types.int (types.listOf types.int); + description = '' + Use `retry.exit_codes` with `retry.max` to retry jobs for only specific failure cases. + ''; + }; + when = mkUnsetOption { + type = types.either types.str (types.listOf types.str); + description = '' + Use `retry.when` with `retry.max` to retry jobs for only specific failure cases. + ''; + }; + max = mkUnsetOption { + type = types.ints.between 0 2; + description = '' + `retry.max` is the maximum number of retries, like retry, and can be 0, 1, or 2. + ''; + }; + }; + }); + description = '' + When and how many times a job can be auto-retried in case of a failure. + + [Docs](https://docs.gitlab.com/ci/yaml/#retry) + ''; + }; + rules = mkUnsetOption { + # could be more granular + type = types.listOf types.attrs; + description = '' + List of conditions to evaluate and determine selected attributes of a job, and whether or not it’s created. + + [Docs](https://docs.gitlab.com/ci/yaml/#rules) + ''; }; script = mkOption { type = types.listOf types.str; + description = '' + Shell script that is executed by a runner. + + [Docs](https://docs.gitlab.com/ci/yaml/#script) + ''; }; - after_script = mkUnsetOption { + secrets = mkUnsetOption { + # could be more granular + type = types.attrs; + description = '' + The CI/CD secrets the job needs. + + [Docs](https://docs.gitlab.com/ci/yaml/#secrets) + ''; + }; + services = mkUnsetOption { + # could be more granular + type = types.attrs; + description = '' + Use Docker services images. + + [Docs](https://docs.gitlab.com/ci/yaml/#services) + ''; + }; + stage = mkOption { + type = types.str; + description = '' + Defines a job stage. + + [Docs](https://docs.gitlab.com/ci/yaml/#stage) + ''; + }; + tags = mkUnsetOption { type = types.listOf types.str; + description = '' + List of tags that are used to select a runner. + + [Docs](https://docs.gitlab.com/ci/yaml/#tags) + ''; }; - artifacts = mkUnsetOption { - type = types.attrs; # TODO: more granular - description = ''''; + timeout = mkUnsetOption { + type = types.str; + description = '' + Define a custom job-level timeout that takes precedence over the project-wide setting. + + [Docs](https://docs.gitlab.com/ci/yaml/#timeout) + ''; }; - rules = mkUnsetOption { - type = types.listOf types.attrs; + trigger = mkUnsetOption { + # could be more granular + type = types.either types.str types.attrs; + description = '' + Defines a downstream pipeline trigger. + + [Docs](https://docs.gitlab.com/ci/yaml/#trigger) + ''; }; - allow_failure = mkUnsetOption { - type = types.bool; + when = mkUnsetOption { + type = types.enum ["on_success" "on_failure" "never" "always" "manual" "delayed"]; + description = '' + When to run job. See also [`manual_confirmation`](#pipelinesnamejobsnamemanual_confirmation) + + [Docs](https://docs.gitlab.com/ci/yaml/#when) + ''; + }; + variables = mkUnsetOption { + type = types.attrsOf types.str; + description = '' + You can use job variables in commands in the job’s `script`, `before_script`, or `after_script` sections, and also with some job keywords. + Check the **Supported values** section of each job keyword to see if it supports variables. + + [Docs](https://docs.gitlab.com/ci/yaml/#job-variables) + ''; }; }; in { diff --git a/lib/impl/modules/pipeline.nix b/lib/impl/modules/pipeline.nix index cfb6284..32b9e0c 100644 --- a/lib/impl/modules/pipeline.nix +++ b/lib/impl/modules/pipeline.nix @@ -4,8 +4,8 @@ jobSubmodule, ... }: let - inherit (lib) mkOption types filterAttrs mergeAttrsList mapAttrs; - inherit (cilib.helpers) filterUnset mkUnsetOption toYaml toYamlPretty; + inherit (lib) mkOption types filterAttrs mergeAttrsList mapAttrs mkRenamedOptionModule literalExpression; + inherit (cilib.helpers) filterUnset unset mkUnsetOption toYaml toYamlPretty eitherWithSubOptions; pipelineConfigSubmodule = {rootConfig, ...}: { options = { @@ -30,19 +30,143 @@ # GITLAB OPTIONS # gitlabOptions = { + default = mkOption { + type = types.submodule { + # allow other keys aswell, maybe define them in the future? https://docs.gitlab.com/ci/yaml/#default + freeformType = types.anything; + options = { + image = mkUnsetOption { + type = types.str; + description = '' + Default image to use for this entire pipeline. + + !!! note + Moved from top level to `default`: [GitLab Docs](https://docs.gitlab.com/ci/yaml/deprecated_keywords/). + ''; + }; + services = mkUnsetOption { + type = types.listOf types.anything; + description = '' + !!! note + Moved from top level to `default`: [GitLab Docs](https://docs.gitlab.com/ci/yaml/deprecated_keywords/). + ''; + }; + cache = mkUnsetOption { + # could be more granular + type = types.either (types.listOf types.attrs) types.attrs; + description = '' + !!! note + Moved from top level to `default`: [GitLab Docs](https://docs.gitlab.com/ci/yaml/deprecated_keywords/). + ''; + }; + before_script = mkUnsetOption { + type = types.listOf types.str; + description = '' + !!! note + Moved from top level to `default`: [GitLab Docs](https://docs.gitlab.com/ci/yaml/deprecated_keywords/). + ''; + }; + after_script = mkUnsetOption { + type = types.listOf types.str; + description = '' + !!! note + Moved from top level to `default`: [GitLab Docs](https://docs.gitlab.com/ci/yaml/deprecated_keywords/). + ''; + }; + }; + }; + # required for it to show up in the docs, but not in the config + default = unset; + defaultText = literalExpression "unset"; + description = '' + Custom default values for job keywords. + ''; + }; + include = mkUnsetOption { + type = types.attrs; + description = '' + Import configuration from other YAML files. + + [Docs](https://docs.gitlab.com/ci/yaml/#include) + ''; + }; stages = mkOption { type = types.listOf types.str; default = []; # .pre and .post always exist apply = val: [".pre"] ++ val ++ [".post"]; + description = '' + The names and order of the pipeline stages. + + [Docs](https://docs.gitlab.com/ci/yaml/#stages) + + !!! note + `.pre` and `.post` are added automatically. + ''; }; variables = mkUnsetOption { - type = types.attrsOf types.str; - description = ''''; + # default/global variables can have descriptions etc. + type = types.attrsOf (eitherWithSubOptions types.str (types.submodule { + options = { + description = mkUnsetOption { + type = types.str; + description = '' + Use the `description` keyword to define a description for a default variable. + ''; + }; + value = mkUnsetOption { + type = types.str; + description = '' + Use the `value` keyword to define a pipeline-level (default) variable’s value. + ''; + }; + options = mkUnsetOption { + type = types.listOf types.str; + description = '' + Use `options` to define an array of values that are [selectable in the UI when running a pipeline manually](https://docs.gitlab.com/ci/pipelines/#configure-a-list-of-selectable-prefilled-variable-values). + ''; + }; + expand = mkUnsetOption { + type = types.bool; + description = '' + Use the `expand` keyword to configure a variable to be expandable or not. + ''; + }; + }; + })); + description = '' + Define default CI/CD variables for all jobs in the pipeline. + Supports strings or attrs as values, for more info see [here](https://docs.gitlab.com/ci/yaml/#variablesdescription). + + [Docs](https://docs.gitlab.com/ci/yaml/#default-variables) + ''; + }; + workflow = mkUnsetOption { + type = types.attrs; + description = '' + Control what types of pipeline run. + + [Docs](https://docs.gitlab.com/ci/yaml/#workflow) + ''; }; }; in { _file = ./pipeline.nix; + # from https://docs.gitlab.com/ci/yaml/#default + imports = builtins.map (val: mkRenamedOptionModule [val] ["default" val]) [ + "after_script" + "artifacts" + "before_script" + "cache" + "hooks" + "id_tokens" + "image" + "interruptible" + "retry" + "services" + "tags" + "timeout" + ]; options = { nix = mkOption { From d88ada2c41ae05b08d7ec644a4d222e93fff78ae Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 25 Sep 2025 14:50:24 +0200 Subject: [PATCH 085/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index 18b1307..0c05817 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.0-alpha.2 +3.0.0-alpha.3 From 046656458b4cbd9d84ef5a5906f30ac303bc9c8e Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 6 Oct 2025 17:53:51 +0200 Subject: [PATCH 086/102] chore: remove old files in preparation for rewrite --- .envrc | 1 - .gitignore | 6 - .gitlab-ci.yml | 49 ---- LICENSE.md | 7 - README.md | 89 ------- docs/caching.md | 50 ---- docs/cicd_component.md | 41 ---- docs/environment_variables.md | 107 --------- docs/examples.md | 18 -- docs/images/favicon.png | Bin 1995 -> 0 bytes docs/images/logo.png | Bin 40993 -> 0 bytes docs/index.md | 11 - docs/kubernetes_runner.md | 38 --- docs/multi_pipeline.md | 31 --- docs/setup.md | 75 ------ docs/usage.md | 33 --- docs/utilities.md | 59 ----- flake.lock | 398 -------------------------------- flake.nix | 304 ------------------------ lib/default.nix | 8 - lib/flake.nix | 8 - lib/flakeModule.nix | 172 -------------- lib/helpers.nix | 49 ---- lib/jobDeps.nix | 36 --- lib/jobPatch.nix | 49 ---- lib/jobRun.nix | 48 ---- lib/pipeline.nix | 61 ----- lib/sandbox_helper.sh | 73 ------ lib/utils.nix | 46 ---- scripts/finalize_nix_ci.sh | 41 ---- scripts/setup_nix_ci.sh | 49 ---- snapshots/default.snap.json | 1 - snapshots/non-default.snap.json | 1 - templates/nix-gitlab-ci.yml | 112 --------- tests/ci-lib.nix | 140 ----------- tests/default.nix | 8 - tests/helpers.nix | 96 -------- tests/pipeline-yamls.nix | 26 --- tests/utils.nix | 36 --- 39 files changed, 2377 deletions(-) delete mode 100644 .envrc delete mode 100644 .gitignore delete mode 100644 .gitlab-ci.yml delete mode 100644 LICENSE.md delete mode 100644 README.md delete mode 100644 docs/caching.md delete mode 100644 docs/cicd_component.md delete mode 100644 docs/environment_variables.md delete mode 100644 docs/examples.md delete mode 100644 docs/images/favicon.png delete mode 100644 docs/images/logo.png delete mode 100644 docs/index.md delete mode 100644 docs/kubernetes_runner.md delete mode 100644 docs/multi_pipeline.md delete mode 100644 docs/setup.md delete mode 100644 docs/usage.md delete mode 100644 docs/utilities.md delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 lib/default.nix delete mode 100644 lib/flake.nix delete mode 100644 lib/flakeModule.nix delete mode 100644 lib/helpers.nix delete mode 100644 lib/jobDeps.nix delete mode 100644 lib/jobPatch.nix delete mode 100644 lib/jobRun.nix delete mode 100644 lib/pipeline.nix delete mode 100644 lib/sandbox_helper.sh delete mode 100644 lib/utils.nix delete mode 100644 scripts/finalize_nix_ci.sh delete mode 100644 scripts/setup_nix_ci.sh delete mode 100644 snapshots/default.snap.json delete mode 100644 snapshots/non-default.snap.json delete mode 100644 templates/nix-gitlab-ci.yml delete mode 100644 tests/ci-lib.nix delete mode 100644 tests/default.nix delete mode 100644 tests/helpers.nix delete mode 100644 tests/pipeline-yamls.nix delete mode 100644 tests/utils.nix diff --git a/.envrc b/.envrc deleted file mode 100644 index f990172..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake . --impure --accept-flake-config diff --git a/.gitignore b/.gitignore deleted file mode 100644 index bbee8ad..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.idea -.devenv -.direnv -.pre-commit-config.yaml -result -*.xml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 24b9507..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,49 +0,0 @@ -include: - - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/nix-gitlab-ci@$CI_COMMIT_SHA - inputs: - version: $CI_COMMIT_SHORT_SHA -stages: - - build-images - - build - - trigger -build:image: - stage: build-images - parallel: - matrix: - - ARCH: ["x86_64-linux", "aarch64-linux"] - image: nixpkgs/nix-flakes:latest - script: - - nix build .#image --system $ARCH - after_script: - - install -D result dist/nix-ci-$ARCH.tar.gz - artifacts: - paths: - - dist -deploy:image: - stage: build-images - image: nixpkgs/nix-flakes:latest - needs: - - build:image - before_script: - - nix profile install nixpkgs#buildah - - export PATH="$PATH:$HOME/.nix-profile/bin" - - export REGISTRY_AUTH_FILE=''${HOME}/auth.json - - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY - - mkdir -p /etc/containers && echo '{"default":[{"type":"insecureAcceptAnything"}]}' > /etc/containers/policy.json - - mkdir -p /var/tmp - script: - - export NORMALIZED_BRANCH=${CI_COMMIT_BRANCH/\//-} - - buildah manifest create localhost/nix-ci - - buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-x86_64-linux.tar.gz - - buildah manifest add localhost/nix-ci docker-archive:dist/nix-ci-aarch64-linux.tar.gz - - buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_SHORT_SHA} - # branches - - | - if [ -z "$CI_COMMIT_TAG" ]; then - buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${NORMALIZED_BRANCH/main/latest} - fi - # tags - - | - if [ -n "$CI_COMMIT_TAG" ]; then - buildah manifest push --all localhost/nix-ci docker://''${CI_REGISTRY_IMAGE}/nix-ci:${CI_COMMIT_TAG} - fi diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 57fd6fa..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2024 TECHNOFAB - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index ceaca42..0000000 --- a/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Nix GitLab CI - -[![built with nix](https://img.shields.io/static/v1?logo=nixos&logoColor=white&label=&message=Built%20with%20Nix&color=41439a)](https://builtwithnix.org) -[![pipeline status](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/badges/main/pipeline.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/commits/main) -![License: MIT](https://img.shields.io/gitlab/license/technofab/nix-gitlab-ci) -[![Latest Release](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/badges/release.svg)](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/releases) -[![Support me](https://img.shields.io/badge/Support-me-orange)](https://tec.tf/#support) -[![Docs](https://img.shields.io/badge/Read-Docs-orange)](https://nix-gitlab-ci.projects.tf) - -Flake module which allows generating a `.gitlab-ci.yml` from Nix. - -This allows easily using any Nix package in CI. - -Also makes it possible to split CI parts in a separate module which can be imported in multiple projects. - -## Usage - -```nix -# flake.nix -{ - ... - inputs.nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; # recommendation: pin to the latest release/version - - outputs = {...}: flake-parts.lib.mkFlake {...} { - imports = [ - inputs.nix-gitlab-ci.flakeModule - ]; - ... - - perSystem = {pkgs, ...}: { - # ci is a shortcut and creates a "default" pipeline - ci = { - stages = ["test"]; - jobs = { - "test" = { - stage = "test"; - nix.deps = [pkgs.unixtools.ping]; - script = [ - "ping -c 5 8.8.8.8" - ]; - }; - }; - }; - # runs on a merge request for example - pipelines."merge_request_event" = { - stages = ["some_stage"]; - jobs = { ... }; - }; - ... - } - } -} -``` - -```yaml -# .gitlab-ci.yml -include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ # recommendation: pin to the latest release/version (don't use "main" etc.) - inputs: - version: # docker image tag, use the same version as a above -``` - -## Utilities - -### Disable Caching temporarily - -To disable any of the provided caches for a pipeline one can set `NIX_CI_DISABLE_CACHE` to -anything non-empty (eg. "yes") when triggering the pipeline. - -The `build:nix-ci` job has a different special environment variable `NIX_CI_FORCE_BUILD` -(useful if the generated pipeline in the cache is outdated, this will build it again). - -### Run Jobs locally - -You can run any job's script (+ before and after) locally with Nix for easier testing: - -```sh -# / pipeline name, like "default" -nix run .#gitlab-ci:pipeline::job: -``` - -There is also `.#gitlab-ci:pipeline::job-deps:` which generates and exports the required environment variables for each job: - -- PATH (with all deps) -- any custom env variables which contain store paths to not break stuff when switching archs - -## Thanks to - -Some parts of this implementation are adapted/inspired from https://gitlab.com/Cynerd/gitlab-ci-nix diff --git a/docs/caching.md b/docs/caching.md deleted file mode 100644 index 0a84110..0000000 --- a/docs/caching.md +++ /dev/null @@ -1,50 +0,0 @@ -# Caching - -Nix GitLab CI supports several caching mechanisms to speed up your pipelines. - -## GitLab Runner Cache - -The runner cache strategy copies the new store paths into a directory `.nix-cache`, -which is then saved in the regular GitLab cache (technically runner cache). -It's also configured as a substituter automatically. - -To enable, set the cache strategy to `runner`. - -Configure it using these environment variables: - -- `RUNNER_CACHE`: path to the runner cache (default `.nix-cache`) - -!!! warning - - This is very inefficient and should probably only be used for very very small - dependency counts. Otherwise it takes an eternity to save to cache. - -## Cachix - -Cachix is a hosted binary cache service that can significantly speed up Nix -builds by sharing build results. - -To enable, set the cache strategy to `attic`. - -Configure it using these environment variables: - -- `CACHIX_CACHE`: name of the cache to use -- (`CACHIX_AUTH_TOKEN`): cachix client itself uses this for authentication - -!!! warning - - Cachix has not been tested. Feedback is appreciated :) - -## Attic (Self-Hosted Cache) - -Attic is a self-hosted, deduplicating binary cache. It's a great option if you -want more control over your caching infrastructure and to have the cache closer -to your runners. - -To enable, set the cache strategy to `attic`. - -Configure it using these environment variables: - -- `ATTIC_SERVER`: URL of the server -- `ATTIC_CACHE`: name of the cache to use -- `ATTIC_TOKEN`: auth token from the attic server diff --git a/docs/cicd_component.md b/docs/cicd_component.md deleted file mode 100644 index 2fe2ae2..0000000 --- a/docs/cicd_component.md +++ /dev/null @@ -1,41 +0,0 @@ -# CI/CD Component - -The CI/CD Component has some inputs which configure defaults for Nix GitLab CI. - -## `version` - -- Type: `string` - -Which version of the Nix CI image to use. Using a tag/version is recommended. -Will not do anything if a custom image is specified using `NIX_CI_IMAGE`. - -## `cache_strategy` - -- Type: `string` -- Default: `"auto"` -- Options: `auto` | `none` | `runner` | `cachix` | `attic` - -Sets the default caching strategy. - -- `auto`: dynamically selects the best strategy for every job based on env variables -- `none`: disables caching -- `runner`, `cachix` & `attic`: forces every job to use this strategy - -Can be overridden by `NIX_CI_CACHE_STRATEGY`, see [Environment Variables](./environment_variables.md). - -## `cache_files` - -- Type: `array` (of strings) -- Default: `["flake.nix", "flake.lock"]` - -Files to use as the cache key for the generated pipeline yaml. -If you use a file like `ci.nix` to define CI, add that here for example. -This makes sure that changes to your Nix CI configuration will invalidate the cache, -otherwise an old pipeline yaml might be used. - -!!! warning - - The value of this is used in `cache:key:files`, which currently only supports - a max of 2 entries. So use something like `["flake.*", "ci.nix"]` to match - `flake.lock`, `flake.nix` and `ci.nix`. - See [gitlab-org/gitlab#301161](https://gitlab.com/gitlab-org/gitlab/-/issues/301161) diff --git a/docs/environment_variables.md b/docs/environment_variables.md deleted file mode 100644 index 56c0ba5..0000000 --- a/docs/environment_variables.md +++ /dev/null @@ -1,107 +0,0 @@ -# Environment Variables - -Nix GitLab CI is mostly controlled using environment variables. -This page outlines all the variables and their use case. - -## `NIX_CI_IMAGE` - -| | | -| ----------- | -------------------------------------------------------------------------- | -| Default | `registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci@$[[ inputs.version ]]` | -| Description | Image to use for the jobs | - -## `NIX_CI_PIPELINE_NAME` - -| | | -| ----------- | ------------------------------------------------- | -| Default | N/A | -| Description | Explicitly request a pipeline to be built and ran | -| See also | [Multi Pipeline](./multi_pipeline.md) | - -## `NIX_CI_DEFAULT_SOURCES` - -| | | -| ----------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Default | `.*` | -| Description | Regex to match `$CI_PIPELINE_SOURCE` against. If it matches, the `default` pipeline will be ran, otherwise `$CI_PIPELINE_SOURCE` | -| See also | [Multi Pipeline](./multi_pipeline.md) | - -## `NIX_CI_FORCE_BUILD` - -| | | -| ----------- | -------------------------------------------------------------------------------------- | -| Default | N/A | -| Description | Set to any non-empty value to force the `nix-ci:build` job to freshly build the config | -| See also | [Caching](./caching.md) | - -## `NIX_CI_DISABLE_CACHE` - -| | | -| ----------- | ------------------------------------------------------ | -| Default | N/A | -| Description | Set to any non-empty value to disable caching for jobs | -| See also | [Caching](./caching.md) | - -## `NIX_CI_CACHE_STRATEGY` - -| | | -| ----------- | --------------------------------------------------------------------------------- | -| Default | `$[[ inputs.cache_strategy ]]` -> defaults to `auto` | -| Description | Caching strategy to use. `auto` will select the strategy based on runner settings | -| See also | [Caching](./caching.md) | - -## `NIX_CI_RUNNER_CACHE_STRATEGY` - -| | | -| ----------- | ---------------------------------------------------------------- | -| Default | N/A | -| Description | Every runner can set it's own preferred cache strategy with this | -| See also | [Caching](./caching.md) | - -## `NIX_CI_DEFAULT_CACHE_STRATEGY` - -| | | -| ----------- | ------------------------------------------------------------------------------------------------- | -| Default | `none` | -| Description | If no runner cache strategy is set and the main strategy is set to auto, this will be the default | -| See also | [Caching](./caching.md) | - -## `RUNNER_CACHE` - -| | | -| ----------- | -------------------------------------- | -| Default | `.nix-cache` | -| Description | Path to directory for the runner cache | -| See also | [Caching](./caching.md) | - -## `CACHIX_CACHE` - -| | | -| ----------- | ------------------------------- | -| Default | N/A | -| Description | Name of the cachix cache to use | -| See also | [Caching](./caching.md) | - -## `ATTIC_CACHE` - -| | | -| ----------- | ------------------------------ | -| Default | N/A | -| Description | Name of the attic cache to use | -| See also | [Caching](./caching.md) | - -## `ATTIC_SERVER` - -| | | -| ----------- | ----------------------- | -| Default | N/A | -| Description | URL of the attic server | -| See also | [Caching](./caching.md) | - -## `ATTIC_TOKEN` - -| | | -| ----------- | ------------------------------- | -| Default | N/A | -| Description | API token from the attic server | -| See also | [Caching](./caching.md) | diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index 6f82a63..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,18 +0,0 @@ -# Example Configs - -## V2 - -- [TECHNOFAB/nix-gitlab-ci](https://gitlab.com/TECHNOFAB/nix-gitlab-ci) - See `flake.nix` for some random example jobs. -- [TECHNOFAB/nixlets](https://gitlab.com/TECHNOFAB/nixlets) -- [TECHNOFAB/nixmkdocs](https://gitlab.com/TECHNOFAB/nixmkdocs) -- [TECHNOFAB/tofunix](https://gitlab.com/TECHNOFAB/tofunix) - -## Old / V1 - -- [TECHNOFAB/coder-templates](https://gitlab.com/TECHNOFAB/coder-templates) - -!!! note - - Feel free to edit this page and add your project if you're using - Nix GitLab CI :) diff --git a/docs/images/favicon.png b/docs/images/favicon.png deleted file mode 100644 index 135b03026d6a5ffb7cbeec80c97224e5b6eb776d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1995 zcmV;+2Q>JJP)Ilb@!-~pgw+p^fd`_57-dFY%EX0E7d_G7}<8 z{n2aqBQ$P)exR!E0QLd}$?1jt8xb1_N?<%jn!WaNwNK(1q7lBs^1kPvDpBkHxAB)z>XxxIyZ|R}wo{HJ=6Dn{QP$^KDk_0i-n|xe#zGce`^2N_Ptw zDEXr1=DW_~F|+p=Ci*yHh#-gvi1COZqY!b4$Pi#E$TNu4h`6^C5qiZ&jdwT$=As_6 zp(v|?4?(}kOzKkXFF;3A6Q-I!0NE&R1t=hx=O$kKqR-&6cb0>(BJJT-(IV_~8%7XI z?yFBu|FPH5s1u-Ft>t^CWj(!CwC+VNWX2%Vhj9s@l!Lmhp3vrv0RDQ zt){tQaM?SFz+^CIqPn@kyfv<0t9!QMeB~yu1ywhR)=dW{0naw{p}YjR#-|qs-9ZfM z&s5EBKFU=v{{T#}ghmgo{$`)5S&dE3`rC2xKW86DwGgGYbxqYCV7#?%immwfwBqtEUqHey!46|F!zx?@zlaR@^9f%hE^4G)@= z^2Sq(K|k`Q8}yIbU-Wr$df`rBw8gJb=#{f9_RMW#@GsdD{?r)%xA8Hr`dEF!&NoL+ zzg3WDynmt?be4}%rY5HsZU9Ev_!R<`-@444da{?piXl6VEsc+H+-lwPD-%nBWFPj# z#PZ`MK!uI>;i7Hb?n3J9-W$Lj%ylRy z7w#B8^U61srL|Wi$a9^#jROT2swl0g#lwEEMG%7`LOE1k!{IAeN$e0#haksIT7z2X z^^o=v-G1D9$!xfCAcRvjwY>FpB~P>syMfDquy#sVYm)#6PUTXPs(Aq9r-E3; zUO=QL%3L7R12D7^V4j~dKoo!x0O4&-LR!M5(*~C>)dR4+M;jiE2)$+Pa)u}o_bZM- zwaJ-Ls`|BfZz`{5M5w+IgQ{7;aFq6-#*G_*u0RW*tU=&9pr=0-k74zxJGb4D@d(kAlMmb!s<`2n2`e zZ8Iw(Qfb}MY-+k-#~UO50c77%yY2L7q(OyW_u?jKG7y>%eP1qlMo|A}T9U zicnpZIkuZ`d%h3Ym~eUS#NxUku|)A_qOH$$-<`VxUD$mtEOBsJ-|b_pL~GxO$|DME z{Z5is#zbFxdE2q+ihkC}^e+G>FneazkRff022I@9>CKOTB|i7=bs%F=t1TPLTLx|h zhBo;*1`F1v#?;=`(+5D@WLIC{pB6w5T?D4(j>tMI=zCVja~gPV*43N?qQ()RyA8}w zfKToHPIhX{XLtQJ6F1o}Xr2{w9{SKQ z>f_d=Jf!~UHSWNGu2qsc+4%8!>r?aW>u0!%#sWDueI-Dj?9`Z==CWX{4IDroVrJo0;=pnpU}rrFnLr}kE0 zo|V}EpCasS)(6bE$*xE%8!PkHrylZ|>k9M)>a9_pw?6gXzI)7Y6+H}usd94d3o&K) d-jDAQ@IM<&9OUDTXUhNp002ovPDHLkV1kV3&P@OS diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 2a70bf66f5680e7b7c454e0a850d5ce731562603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40993 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 # example: http://atticd..svc.cluster.local:8080 -ATTIC_CACHE: ci # name however you want, just needs to exist -ATTIC_TOKEN: -``` - -... makes your jobs automatically cache their Nix store paths to the in-cluster -attic when running with this runner. - -Other runners could use cachix or no cache, you get the idea ;P - -!!! note - - This of course works with any executor where you can set environment - variables. This is just an example how to do it in Kubernetes easily. diff --git a/docs/multi_pipeline.md b/docs/multi_pipeline.md deleted file mode 100644 index d8dbfe6..0000000 --- a/docs/multi_pipeline.md +++ /dev/null @@ -1,31 +0,0 @@ -# Multiple Pipelines - -With V2, Nix GitLab CI can generate different pipelines, depending on the -pipeline source (`$CI_PIPELINE_SOURCE`). - -By default, no matter which source, the `default` pipeline is built and ran. -`$NIX_CI_PIPELINE` can override that, eg. when manually triggering a run. -To configure which source should be 1-to-1 translated to a pipeline with the -same name, set `$NIX_CI_DEFAULT_SOURCES` to a regex which explicitly does not -match these sources. Or set it to an impossible to match regex, then it will -always run the pipeline named after `$CI_PIPELINE_SOURCE`. - -## Example 1: always run default - -If you only have a single pipeline, you just have to call it `default`. -Everything else works out of the box. - -## Example 2: default and merge_request_event - -If you want the source `merge_request_event` to trigger a different pipeline, -name it like that and set `$NIX_CI_DEFAULT_SOURCES` to `^(merge_request_event)$`. -Now a merge request will run this pipeline, while everything else runs `default`. - -## Example 3: default, push and web - -Set `$NIX_CI_DEFAULT_SOURCES` to `^(push|web)$`. - -## Example 4: always run the specific pipelines, never default - -Set `$NIX_CI_DEFAULT_SOURCES` to any regex that never matches the sources, -like `a\A` or `nothing`. diff --git a/docs/setup.md b/docs/setup.md deleted file mode 100644 index d3aa96e..0000000 --- a/docs/setup.md +++ /dev/null @@ -1,75 +0,0 @@ -# Setup - -To integrate Nix GitLab CI into your project, you need to make two main changes: - -1. Add the `nix-gitlab-ci` flake module to your `flake.nix`. -1. Include the necessary component in your `.gitlab-ci.yml`. - -## Adding to `flake.nix` - -In your project's `flake.nix`, add `nix-gitlab-ci` as an input and import its -flake module within your `flake-parts` configuration. - -```nix title="flake.nix" -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Or your preferred nixpkgs branch/version - flake-parts.url = "github:hercules-ci/flake-parts"; - - # Add nix-gitlab-ci as an input - # recommendation: pin to a specific release/version - nix-gitlab-ci.url = "gitlab:TECHNOFAB/nix-gitlab-ci/?dir=lib"; - }; - - outputs = { nixpkgs, flake-parts, ... }@inputs: - flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ - # Import the nix-gitlab-ci flake module - inputs.nix-gitlab-ci.flakeModule - ]; - - systems = [ - "x86_64-linux" - "aarch64-linux" - # Add other systems you need - ]; - - perSystem = { pkgs, ... }: { - # define your CI pipelines - # ci = { ... }; - # pipelines."merge_request_event" = { ... }; - }; - }; -} -``` - -Replace `` with the specific version or commit hash of `nix-gitlab-ci` -you wish to use. Pinning to a specific version is highly recommended for -reproducibility and compatibility. - -!!! warning - - While the flake input is locked through `flake.lock`, the CI/CD component - will always use the latest commit of the reference. This means that by using - a branch like `main` as version for both, the CI/CD component will always use - the latest commit while your flake uses a fixed one. - This could result in drift between both, potentially breaking stuff. - -## Including in `.gitlab-ci.yml` - -Your `.gitlab-ci.yml` file will be minimal. Its primary role is to include the -`nix-gitlab-ci` component, which will then generate the full CI configuration -based on your Nix code. - -```yaml title=".gitlab-ci.yml" -include: - - component: gitlab.com/TECHNOFAB/nix-gitlab-ci/nix-gitlab-ci@ - inputs: - # This input sets the Docker image tag used for the CI jobs. - # Use the same version as you pinned in your flake.nix for consistency. - version: -``` - -Again, ensure `` matches the version used in your `flake.nix`. -This component includes a job (`build:nix-ci`) that will evaluate your Nix -configuration and generate the `.gitlab-ci.yml` used for the pipeline run. diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index b28a755..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,33 +0,0 @@ -# Usage - -To create a basic pipeline, configure it by setting `ci` in `perSystem`. -The schema is similar to the `.gitlab-ci.yml`, only jobs are defined differently: - -```nix -ci = { - # Nix GitLab CI specific config, see `configType` in `flakeModule.nix` - config = {}; - jobs = { - "job-a" = {}; - "job-b" = {}; - }; -}; -``` - -For every job, there are a couple of settings you can adjust aswell: - -```nix -"job-a" = { - # see `jobType` in `flakeModule.nix` - nix = { - enable = true; # is this a nix-based job? - deps = []; # dependencies to install for this job - # for gitlab runner cache: - enable-runner-cache = false; - runner-cache-key = ""; - }; -}; -``` - -Since V2 multiple pipelines are supported. -See [Multiple Pipelines](./multi_pipeline.md) for more. diff --git a/docs/utilities.md b/docs/utilities.md deleted file mode 100644 index e5a7b96..0000000 --- a/docs/utilities.md +++ /dev/null @@ -1,59 +0,0 @@ -# Utilities - -Nix GitLab CI provides a couple of utilities to help with development and -debugging. - -## Disabling caching temporarily - -Nix GitLab CI often utilizes caching mechanisms to speed up your pipelines -(see [Caching](./caching.md)). -However, there might be situations where you need to temporarily disable these -caches for a specific pipeline run, for example, to debug a caching issue or -ensure a clean build. - -To disable most of the provided caches for a pipeline, set the environment -variable `NIX_CI_DISABLE_CACHE` to any non-empty value (e.g., `yes`, `true`, `1`) -when triggering the pipeline in the GitLab UI or via the API. - -## Forcing a rebuild of the CI pipeline definition - -The job responsible for generating the `.gitlab-ci.yml` from your Nix code -(`build:nix-ci`) might itself be cached. If you've made changes to your Nix CI -configuration and the pipeline doesn't seem to pick them up, the cached job -definition might be the reason. - -You should first double check if all the Nix files you defined the CI config in -are specified in the `cache_files` CI/CD-component input -(see [CI/CD Component](./cicd_component.md) for more). - -To force this specific job to rebuild and re-evaluate your Nix configuration, -set the environment variable `NIX_CI_FORCE_BUILD` when triggering the pipeline. - -## Running jobs locally - -One of the benefits of defining your CI jobs with Nix is the ability to run them -locally in an environment that closely mirrors the CI environment. This can -significantly speed up debugging and development. - -You can run the script of any defined job locally using the `nix run` command. -The syntax is: - -```sh -nix run .#gitlab-ci:pipeline::job: -``` - -Replace `` with the name of the pipeline the job belongs to -(e.g., `default` for jobs defined under the `ci` attribute) and `` -with the name of the job you want to run. - -This command will set up the environment with the specified `nix.deps` and -execute the job's `script`. - -There is also an attribute `.#gitlab-ci:pipeline::job-deps:`. -Building this derivation will generate a shell script which exports the required -environment variables for the job, such as the `PATH` including all dependencies -and any custom environment variables that contain store paths (ensuring they are -correctly resolved across different architectures). - -You can use this to inspect the environment that would be set up for a job without -running the full script. diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 38bc581..0000000 --- a/flake.lock +++ /dev/null @@ -1,398 +0,0 @@ -{ - "nodes": { - "cachix": { - "inputs": { - "devenv": [ - "devenv" - ], - "flake-compat": [ - "devenv" - ], - "git-hooks": [ - "devenv" - ], - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1737621947, - "narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=", - "owner": "cachix", - "repo": "cachix", - "rev": "f65a3cd5e339c223471e64c051434616e18cc4f5", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "latest", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat", - "git-hooks": "git-hooks", - "nix": "nix", - "nixpkgs": "nixpkgs_3" - }, - "locked": { - "lastModified": 1743687534, - "narHash": "sha256-QIh5jKWE0aZ4N77Zu3kz0RjA22zqqy5p6PfJnOW5rPE=", - "owner": "cachix", - "repo": "devenv", - "rev": "efd5d68a6483573410565d0b940e1a67b6f92591", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "devenv", - "nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1712014858, - "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_2": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1743550720, - "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "c621e8422220273271f52058f618c94e405bb0f5", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": [ - "devenv" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1740849354, - "narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "libgit2": { - "flake": false, - "locked": { - "lastModified": 1697646580, - "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", - "owner": "libgit2", - "repo": "libgit2", - "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", - "type": "github" - }, - "original": { - "owner": "libgit2", - "repo": "libgit2", - "type": "github" - } - }, - "mkdocs-material-umami": { - "locked": { - "lastModified": 1745840856, - "narHash": "sha256-1Ad1JTMQMP6YsoIKAA+SBCE15qWrYkGue9/lXOLnu9I=", - "owner": "technofab", - "repo": "mkdocs-material-umami", - "rev": "3ac9b194450f6b779c37b8d16fec640198e5cd0a", - "type": "gitlab" - }, - "original": { - "owner": "technofab", - "repo": "mkdocs-material-umami", - "type": "gitlab" - } - }, - "nix": { - "inputs": { - "flake-compat": [ - "devenv" - ], - "flake-parts": "flake-parts", - "libgit2": "libgit2", - "nixpkgs": "nixpkgs_2", - "nixpkgs-23-11": [ - "devenv" - ], - "nixpkgs-regression": [ - "devenv" - ], - "pre-commit-hooks": [ - "devenv" - ] - }, - "locked": { - "lastModified": 1741798497, - "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", - "owner": "domenkozar", - "repo": "nix", - "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.24", - "repo": "nix", - "type": "github" - } - }, - "nix-mkdocs": { - "locked": { - "dir": "lib", - "lastModified": 1745175318, - "narHash": "sha256-eJOTw3SK4psqBPP2hNJ4D/13/oi/FLoM0tYKoCGVFP8=", - "owner": "technofab", - "repo": "nixmkdocs", - "rev": "6d6e0139060c896ae14de4b9c82335655a384643", - "type": "gitlab" - }, - "original": { - "dir": "lib", - "owner": "technofab", - "repo": "nixmkdocs", - "type": "gitlab" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1733212471, - "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "lastModified": 1743296961, - "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1717432640, - "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "release-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1733477122, - "narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1745377448, - "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { - "locked": { - "lastModified": 1735554305, - "narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixtest": { - "locked": { - "dir": "lib", - "lastModified": 1748945995, - "narHash": "sha256-OWflapMq78nCUT7N0vS/AuK4E8y7p8oh0zjnDioJ4Lw=", - "owner": "technofab", - "repo": "nixtest", - "rev": "392e8796f386373a36aecd3216925accf2714a1a", - "type": "gitlab" - }, - "original": { - "dir": "lib", - "owner": "technofab", - "ref": "1.0.0", - "repo": "nixtest", - "type": "gitlab" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "flake-parts": "flake-parts_2", - "mkdocs-material-umami": "mkdocs-material-umami", - "nix-mkdocs": "nix-mkdocs", - "nixpkgs": "nixpkgs_4", - "nixtest": "nixtest", - "systems": "systems", - "treefmt-nix": "treefmt-nix" - } - }, - "systems": { - "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", - "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default-linux", - "type": "github" - } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": "nixpkgs_5" - }, - "locked": { - "lastModified": 1743677901, - "narHash": "sha256-eWZln+k+L/VHO69tUTzEmgeDWNQNKIpSUa9nqQgBrSE=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "57dabe2a6255bd6165b2437ff6c2d1f6ee78421a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 0788dc0..0000000 --- a/flake.nix +++ /dev/null @@ -1,304 +0,0 @@ -{ - outputs = { - flake-parts, - systems, - ... - } @ inputs: - flake-parts.lib.mkFlake {inherit inputs;} { - imports = [ - inputs.devenv.flakeModule - inputs.treefmt-nix.flakeModule - inputs.nix-mkdocs.flakeModule - inputs.nixtest.flakeModule - ./lib/flakeModule.nix - ]; - systems = import systems; - flake = {}; - perSystem = { - pkgs, - config, - system, - ... - }: rec { - imports = [ - ./tests - ]; - treefmt = { - projectRootFile = "flake.nix"; - programs = { - alejandra.enable = true; - mdformat.enable = true; - yamlfmt.enable = true; - }; - settings.formatter = { - yamlfmt.excludes = ["templates/nix-gitlab-ci.yml"]; - mdformat.command = let - pkg = pkgs.python3.withPackages (p: [ - p.mdformat - p.mdformat-mkdocs - ]); - in "${pkg}/bin/mdformat"; - }; - }; - devenv.shells.default = { - containers = pkgs.lib.mkForce {}; - packages = with pkgs; [dive skopeo]; - - pre-commit = { - hooks = { - treefmt = { - enable = true; - packageOverrides.treefmt = config.treefmt.build.wrapper; - }; - }; - }; - }; - doc = { - path = ./docs; - deps = pp: [ - pp.mkdocs-material - (pp.callPackage inputs.mkdocs-material-umami {}) - ]; - config = { - site_name = "Nix GitLab CI"; - repo_name = "TECHNOFAB/nix-gitlab-ci"; - repo_url = "https://gitlab.com/TECHNOFAB/nix-gitlab-ci"; - edit_uri = "edit/main/docs/"; - theme = { - name = "material"; - features = ["content.code.copy" "content.action.edit"]; - icon.repo = "simple/gitlab"; - logo = "images/logo.png"; - favicon = "images/favicon.png"; - palette = [ - { - scheme = "default"; - media = "(prefers-color-scheme: light)"; - primary = "deep orange"; - accent = "orange"; - toggle = { - icon = "material/brightness-7"; - name = "Switch to dark mode"; - }; - } - { - scheme = "slate"; - media = "(prefers-color-scheme: dark)"; - primary = "deep orange"; - accent = "orange"; - toggle = { - icon = "material/brightness-4"; - name = "Switch to light mode"; - }; - } - ]; - }; - plugins = ["search" "material-umami"]; - nav = [ - {"Introduction" = "index.md";} - {"Setup" = "setup.md";} - {"Usage" = "usage.md";} - {"CI/CD Component" = "cicd_component.md";} - {"Environment Variables" = "environment_variables.md";} - {"Caching" = "caching.md";} - {"Multiple Pipelines" = "multi_pipeline.md";} - {"Utilities" = "utilities.md";} - {"Kubernetes Runner Example" = "kubernetes_runner.md";} - {"Example Configs" = "examples.md";} - ]; - markdown_extensions = [ - { - "pymdownx.highlight".pygments_lang_class = true; - } - "pymdownx.inlinehilite" - "pymdownx.snippets" - "pymdownx.superfences" - "fenced_code" - "admonition" - ]; - extra.analytics = { - provider = "umami"; - site_id = "28f7c904-db22-4c2b-9ee4-ed42e14b6db9"; - src = "https://analytics.tf/umami"; - domains = "nix-gitlab-ci.projects.tf"; - feedback = { - title = "Was this page helpful?"; - ratings = [ - { - icon = "material/thumb-up-outline"; - name = "This page is helpful"; - data = "good"; - note = "Thanks for your feedback!"; - } - { - icon = "material/thumb-down-outline"; - name = "This page could be improved"; - data = "bad"; - note = "Thanks for your feedback! Please leave feedback by creating an issue :)"; - } - ]; - }; - }; - }; - }; - # should set the "default" pipeline - ci = { - stages = ["test" "nixtest" "build" "deploy"]; - jobs = { - "test" = { - stage = "test"; - nix = { - deps = [pkgs.hello pkgs.curl]; - enable-runner-cache = true; - }; - variables = { - TEST = "test"; - TEST_WITH_DERIVATION = "${pkgs.hello}/test"; - }; - script = [ - "hello" - "curl google.de" - "echo $TEST $TEST_WITH_DERIVATION" - ]; - }; - "test-default" = { - stage = "test"; - nix.deps = [pkgs.hello]; - script = ["hello"]; - }; - "test-non-nix" = { - nix.enable = false; - stage = "test"; - image = "alpine:latest"; - script = [ - "echo \"This job will not be modified to use nix\"" - ]; - }; - # -- actually useful jobs -- - "docs" = { - stage = "build"; - script = [ - # sh - '' - nix build .#docs:default - mkdir -p public - cp -r result/. public/ - '' - ]; - artifacts.paths = ["public"]; - }; - "pages" = { - nix.enable = false; - image = "alpine:latest"; - stage = "deploy"; - script = ["true"]; - artifacts.paths = ["public"]; - rules = [ - { - "if" = "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"; - } - ]; - }; - "nixtest" = { - stage = "nixtest"; - script = [ - # sh - "nix run .#nixtests:run -- --junit=junit.xml --pure" - ]; - allow_failure = true; - artifacts = { - when = "always"; - reports.junit = "junit.xml"; - }; - }; - }; - }; - pipelines."non-default" = { - stages = ["test"]; - jobs = { - "test" = { - stage = "test"; - script = [ - "echo Hello from another pipeline" - ]; - }; - }; - }; - - packages = let - setupScript = pkgs.writeShellScriptBin "setup_nix_ci" (builtins.readFile ./scripts/setup_nix_ci.sh); - finalizeScript = pkgs.writeShellScriptBin "finalize_nix_ci" (builtins.readFile ./scripts/finalize_nix_ci.sh); - in { - setup-script = setupScript; - finalize-script = finalizeScript; - image = pkgs.dockerTools.buildImage { - name = "nix-ci"; - fromImage = let - hashes = { - "x86_64-linux" = "sha256-kJ7dqje5o1KPr3RDZ7/THbhMSoiCU1C/7HshDrNfwnM="; - "aarch64-linux" = "sha256-jz+Z3Ji+hy5d9ImOh/YOKCqy9P9/cseSov+5J/O95bg="; - }; - # check digest of tags like nixos-24.11-aarch64-linux etc. - digests = { - "x86_64-linux" = "sha256:345f210dea4cbd049e2d01d13159c829066dfb6e273cdd49ea878186d17b19f7"; - "aarch64-linux" = "sha256:66163fdf446d851416dd4e9be28c0794d9c2550214a57a846957699a3f5747f6"; - }; - hash = hashes.${system} or (throw "Unsupported system"); - imageDigest = digests.${system} or (throw "Unsupported system"); - in - pkgs.dockerTools.pullImage { - imageName = "nixpkgs/nix-flakes"; - inherit hash imageDigest; - }; - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = with pkgs; - [ - gitMinimal - gnugrep - gnused - coreutils - diffutils - cachix - attic-client - ] - ++ [ - setupScript - finalizeScript - ]; - pathsToLink = ["/bin"]; - }; - }; - }; - - checks = packages; - }; - }; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - # flake & devenv related - flake-parts.url = "github:hercules-ci/flake-parts"; - systems.url = "github:nix-systems/default-linux"; - devenv.url = "github:cachix/devenv"; - treefmt-nix.url = "github:numtide/treefmt-nix"; - nix-mkdocs.url = "gitlab:technofab/nixmkdocs?dir=lib"; - mkdocs-material-umami.url = "gitlab:technofab/mkdocs-material-umami"; - nixtest.url = "gitlab:technofab/nixtest/1.0.0?dir=lib"; - }; - - nixConfig = { - extra-substituters = [ - "https://cache.nixos.org/" - "https://nix-community.cachix.org" - "https://devenv.cachix.org" - ]; - - extra-trusted-public-keys = [ - "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" - "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" - "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" - ]; - }; -} diff --git a/lib/default.nix b/lib/default.nix deleted file mode 100644 index 37f6318..0000000 --- a/lib/default.nix +++ /dev/null @@ -1,8 +0,0 @@ -args: { - helpers = import ./helpers.nix args; - jobDeps = import ./jobDeps.nix args; - jobRun = import ./jobRun.nix args; - jobPatch = import ./jobPatch.nix args; - pipeline = import ./pipeline.nix args; - utils = import ./utils.nix args; -} diff --git a/lib/flake.nix b/lib/flake.nix deleted file mode 100644 index b2cc77a..0000000 --- a/lib/flake.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ - description = "Nix-GitLab-CI lib"; - - outputs = {...}: { - flakeModule = import ./flakeModule.nix; - lib = import ./default.nix; - }; -} diff --git a/lib/flakeModule.nix b/lib/flakeModule.nix deleted file mode 100644 index 4f8a274..0000000 --- a/lib/flakeModule.nix +++ /dev/null @@ -1,172 +0,0 @@ -{ - flake-parts-lib, - lib, - ... -}: { - options.perSystem = flake-parts-lib.mkPerSystemOption ( - { - config, - pkgs, - ... - }: let - cilib = import ./. {inherit lib pkgs;}; - inherit (cilib.pipeline) mkPipeline; - inherit (lib) types mkOption; - - cfg = config.ci.config; - - subType = options: types.submodule {inherit options;}; - mkNullOption = type: - mkOption { - default = null; - type = types.nullOr type; - }; - - configType = subType { - nix-jobs-by-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"; - }; - }; - jobType = subType { - # nix ci opts - nix = mkOption { - type = subType { - enable = mkOption { - type = types.bool; - default = cfg.nix-jobs-by-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"; - }; - enable-runner-cache = mkOption { - type = types.bool; - default = false; - description = '' - Cache this job using the GitLab Runner cache. - Warning: useful for tiny jobs, but most of the time it just takes an eternity. - ''; - }; - runner-cache-key = mkOption { - type = types.str; - default = "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"; - description = "Cache key to use for the runner nix cache. Requires enable-runner-cache = true"; - }; - }; - 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 = "$NIX_CI_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); - }; - - ciType = 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 = {}; - }; - }; - in { - options = { - pipelines = mkOption { - type = types.lazyAttrsOf ciType; - description = '' - Create multiple GitLab CI pipelines. - - See README.md for more information about how a pipeline is selected. - ''; - default = {}; - apply = op: let - # NOTE: show warning if "default" is set and config.ci is not {} - legacyMode = config.ci != {}; - defaultExists = builtins.hasAttr "default" op; - value = - { - "default" = config.ci; - } - // op; - in - if defaultExists && legacyMode - then builtins.trace "Warning: config.ci is overwritten by pipelines.default" value - else value; - }; - ci = mkOption { - type = ciType; - description = '' - Note: this is a shorthand for writing `pipelines."default"` - - 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.legacyPackages = lib.fold (pipeline: acc: acc // pipeline) {} ( - map ( - pipeline_name: - (mkPipeline { - pipeline = config.pipelines."${pipeline_name}"; - name = pipeline_name; - }).packages - ) (builtins.attrNames config.pipelines) - ); - } - ); -} diff --git a/lib/helpers.nix b/lib/helpers.nix deleted file mode 100644 index 3b4c057..0000000 --- a/lib/helpers.nix +++ /dev/null @@ -1,49 +0,0 @@ -{lib, ...} @ args: let - inherit (lib) isAttrs filterAttrs mapAttrs; -in rec { - prepend = key: arr: job: - job - // lib.optionalAttrs (job.nix.enable or false) { - ${key} = - arr - ++ (job.${key} or []); - }; - append = key: arr: job: - job - // lib.optionalAttrs (job.nix.enable or false) { - ${key} = (job.${key} or []) ++ arr; - }; - prependToBeforeScript = prepend "before_script"; - appendToAfterScript = append "after_script"; - - # json is also valid yaml and this removes dependency on jq and/or remarshal - # (used in pkgs.formats.json and pkgs.formats.yaml respectively) - toYaml = name: value: builtins.toFile name (builtins.toJSON value); - - customMapAttrs = cb: set: builtins.listToAttrs (builtins.map (key: cb key (builtins.getAttr key set)) (builtins.attrNames set)); - - filterAttrsRec = pred: v: - if isAttrs v - then filterAttrs pred (mapAttrs (path: filterAttrsRec pred) v) - else v; - - # 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 {}); - - # args.pkgs so "pkgs" does not need to be passed all the time - stdenvMinimal = args.pkgs.stdenvNoCC.override { - cc = null; - preHook = ""; - allowedRequisites = null; - initialPath = with args.pkgs; [coreutils findutils]; - extraNativeBuildInputs = []; - }; -} diff --git a/lib/jobDeps.nix b/lib/jobDeps.nix deleted file mode 100644 index 27bad19..0000000 --- a/lib/jobDeps.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./. {inherit lib pkgs;}; - inherit (cilib.helpers) filterJobVariables stdenvMinimal; -in { - mkJobDeps = { - key, - job, - }: let - variablesWithStorePaths = filterJobVariables true job; - variableExports = lib.concatLines ( - lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithStorePaths - ); - script = '' - export PATH="${lib.makeBinPath (job.nix.deps or [])}:$PATH"; - # variables containing nix derivations: - ${variableExports} - ''; - in - stdenvMinimal.mkDerivation { - name = "gitlab-ci-job-deps-${key}"; - dontUnpack = true; - installPhase = - # sh - '' - echo '${script}' > $out - chmod +x $out - ''; - passthru = { - inherit script; - }; - }; -} diff --git a/lib/jobPatch.nix b/lib/jobPatch.nix deleted file mode 100644 index 959a9c0..0000000 --- a/lib/jobPatch.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./. {inherit lib pkgs;}; - inherit (lib) toList; - inherit (cilib.helpers) prependToBeforeScript appendToAfterScript filterJobVariables; -in { - mkJobPatched = { - key, - job, - pipeline_name, - }: - builtins.removeAttrs ( - (prependToBeforeScript [ - "source setup_nix_ci \"gitlab-ci:pipeline:${pipeline_name}:job-deps:${key}\"" - ] - (appendToAfterScript [ - "finalize_nix_ci" - ] - job)) - // lib.optionalAttrs job.nix.enable ( - (let - variables = - (filterJobVariables false job) - // lib.optionalAttrs job.nix.enable-runner-cache { - NIX_CI_CACHE_STRATEGY = "runner"; - }; - in - # filter empty variables - lib.optionalAttrs (variables != {}) { - inherit variables; - }) - // (let - cache = - (toList (job.cache or [])) - ++ (lib.optional (job.nix.enable-runner-cache) { - key = job.nix.runner-cache-key; - paths = [".nix-cache/"]; - }); - in - # filter empty cache - lib.optionalAttrs (cache != []) { - inherit cache; - }) - ) - ) ["nix"]; -} diff --git a/lib/jobRun.nix b/lib/jobRun.nix deleted file mode 100644 index 5c4a118..0000000 --- a/lib/jobRun.nix +++ /dev/null @@ -1,48 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./. {inherit lib pkgs;}; - inherit (cilib.helpers) filterJobVariables; -in { - mkJobRun = { - key, - job, - jobDeps, - }: let - variablesWithoutStorePaths = filterJobVariables false job; - variableExports = lib.concatLines ( - lib.mapAttrsToList (name: value: "export ${name}=\"${value}\"") variablesWithoutStorePaths - ); - sandboxHelper = pkgs.writeShellScriptBin "gitlab-ci-job-sandbox-helper" (builtins.readFile ./sandbox_helper.sh); - actualJobScript = pkgs.writeShellScript "gitlab-ci-job:${key}:raw" '' - # set up deps and environment variables containing store paths - . ${jobDeps} - # normal environment variables - ${variableExports} - # run before_script, script and after_script - echo -e "\e[32mRunning before_script...\e[0m" - set -x - ${lib.concatLines (job.before_script or [])} - { set +x; } 2>/dev/null - echo -e "\e[32mRunning script...\e[0m" - set -x - ${lib.concatLines job.script} - { set +x; } 2>/dev/null - echo -e "\e[32mRunning after_script...\e[0m" - set -x - ${lib.concatLines (job.after_script or [])} - { set +x; } 2>/dev/null - ''; - in - # this way the sandbox helper just needs to be built once - pkgs.writeShellScriptBin "gitlab-ci-job:${key}" '' - exec ${lib.getExe sandboxHelper} ${actualJobScript} $@ - '' - // { - passthru = { - inherit jobDeps actualJobScript; - }; - }; -} diff --git a/lib/pipeline.nix b/lib/pipeline.nix deleted file mode 100644 index 2873b22..0000000 --- a/lib/pipeline.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./. {inherit lib pkgs;}; - inherit (cilib.helpers) filterAttrsRec customMapAttrs toYaml; - inherit (cilib.jobDeps) mkJobDeps; - inherit (cilib.jobRun) mkJobRun; - inherit (cilib.jobPatch) mkJobPatched; -in { - mkPipeline = { - name, - pipeline, - }: let - jobs = filterAttrsRec (n: v: v != null) pipeline.jobs; - rest = filterAttrsRec (n: v: v != null) (builtins.removeAttrs pipeline ["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 = - customMapAttrs (key: job: { - name = "gitlab-ci:pipeline:${name}:job-deps:${key}"; - value = mkJobDeps {inherit key job;}; - }) - jobs; - # allows the user to directly run the script - jobsMappedForScript = - customMapAttrs (key: job: { - name = "gitlab-ci:pipeline:${name}:job:${key}"; - value = mkJobRun { - inherit key job; - jobDeps = jobsMappedForDeps."gitlab-ci:pipeline:${name}:job-deps:${key}"; - }; - }) - jobs; - # build the deps specific for this job before anything, this way the deps should be fetched from the cache - jobsPatched = - customMapAttrs (key: job: { - name = key; - value = assert lib.assertMsg (builtins.elem job.stage (rest.stages or [])) "stage '${job.stage}' of job '${key}' does not exist"; - mkJobPatched { - inherit key job; - pipeline_name = name; - }; - }) - jobs; - in { - packages = - # gitlab-ci:pipeline: - # gitlab-ci:pipeline::job: - # gitlab-ci:pipeline::job-deps: - { - "gitlab-ci:pipeline:${name}" = toYaml "gitlab-ci-${name}.yml" (rest // jobsPatched); - } - // jobsMappedForDeps - // jobsMappedForScript; - finalConfig = rest // jobsPatched; - }; -} diff --git a/lib/sandbox_helper.sh b/lib/sandbox_helper.sh deleted file mode 100644 index 4ea5ec5..0000000 --- a/lib/sandbox_helper.sh +++ /dev/null @@ -1,73 +0,0 @@ -echo -e "\e[32mSetting up...\e[0m" - -actualJobScript=$1 -shift - -INCLUDE_DIRTY=false -NO_SANDBOX=false -KEEP_TMP=false -KEEP_ENV="" -# parse flags -while [[ $# -gt 0 ]]; do - case "$1" in - --include-dirty) - INCLUDE_DIRTY=true - shift - ;; - --no-sandbox) - NO_SANDBOX=true - shift - ;; - --keep-tmp) - KEEP_TMP=true - shift - ;; - --keep-env) - KEEP_ENV="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac -done - -if [ "$NO_SANDBOX" = false ]; then - echo "Running with simple sandboxing" - if [ "$KEEP_TMP" = false ]; then - trap "rm -rf '$TMPDIR'" EXIT - else - echo "Temp dir will be preserved at: $TMPDIR" - fi - - # check if dirty - DIRTY_PATCH="" - if ! git diff --quiet && ! git diff --staged --quiet; then - echo "Warning: working tree is dirty." - DIRTY_PATCH=$(mktemp -t "nix-gitlab-ci.XXX.patch") - git diff --staged > "$DIRTY_PATCH" - trap "rm -f '$DIRTY_PATCH'" EXIT - fi - TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") - git clone . $TMPDIR - pushd $TMPDIR >/dev/null - if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then - echo "Copying dirty changes..." - git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" - fi - - echo "Running job in $TMPDIR" - env -i $( - if [[ -n "$KEEP_ENV" ]]; then - IFS=',' read -ra VARS <<< "$KEEP_ENV" - for var in "${VARS[@]}"; do - printf '%s=%q ' "$var" "${!var}" - done - fi - ) bash $actualJobScript - popd >/dev/null -else - exec $actualJobScript -fi - diff --git a/lib/utils.nix b/lib/utils.nix deleted file mode 100644 index ae15284..0000000 --- a/lib/utils.nix +++ /dev/null @@ -1,46 +0,0 @@ -{pkgs, ...}: { - commitAndPushFiles = { - message, - files ? [], - }: jobArgs: - jobArgs - // { - before_script = - (jobArgs.before_script or []) - ++ [ - # sh - '' - echo -e "\\e[0Ksection_start:`date +%s`:commit_setup[collapsed=true]\\r\\e[0KSetting up commitAndPushFiles" - eval "$(ssh-agent -s)" >/dev/null; - mkdir -p ~/.ssh; touch ~/.ssh/known_hosts; - ssh-keyscan -t rsa $CI_SERVER_HOST >> ~/.ssh/known_hosts; - echo "$GIT_SSH_PRIV_KEY" | tr -d '\r' | ssh-add - >/dev/null; - git config --global user.email "$GIT_EMAIL" >/dev/null; - git config --global user.name "$GIT_NAME" >/dev/null; - export CI_PUSH_REPO=`echo $CI_REPOSITORY_URL | sed -e "s|.*@\(.*\)|git@\1|" -e "s|/|:|"`; - git remote rm origin && git remote add origin ''${CI_PUSH_REPO} - echo -e "\\e[0Ksection_end:`date +%s`:commit_setup\\r\\e[0K" - '' - ]; - script = let - addScript = - if builtins.length files == 0 - then "" - else "git add ${builtins.concatStringsSep " " files}"; - in - (jobArgs.script or []) - ++ [ - # sh - '' - echo -e "\\e[0Ksection_start:`date +%s`:commit[collapsed=true]\\r\\e[0KCommiting & pushing changes if necessary" - ${addScript} - git diff --cached --exit-code >/dev/null && - echo "Nothing to commit" || - git commit -m "${message}" --no-verify; - git push --tags origin ''${GIT_SOURCE_REF:-HEAD}:''${GIT_TARGET_REF:-$CI_COMMIT_REF_NAME} -o ci.skip - echo -e "\\e[0Ksection_end:`date +%s`:commit\\r\\e[0K" - '' - ]; - nix.deps = (jobArgs.nix.deps or []) ++ [pkgs.openssh pkgs.gitMinimal pkgs.gnused]; - }; -} diff --git a/scripts/finalize_nix_ci.sh b/scripts/finalize_nix_ci.sh deleted file mode 100644 index baa6a0d..0000000 --- a/scripts/finalize_nix_ci.sh +++ /dev/null @@ -1,41 +0,0 @@ -echo -e "\\e[0Ksection_start:`date +%s`:finalize_nix_ci[collapsed=true]\\r\\e[0KFinalizing Nix CI..." - nix path-info --all > /tmp/nix-store-after - echo "Finding new paths..." - NEW_PATHS=$(diff --new-line-format="%L" \ - --old-line-format="" --unchanged-line-format="" \ - /tmp/nix-store-before /tmp/nix-store-after) - COUNT=$(wc -l <<<"$NEW_PATHS") - - if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then - export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; - fi - - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_push[collapsed=true]\\r\\e[0KPushing $COUNT new store paths to cache ($NIX_CI_CACHE_STRATEGY)" - echo -n "$NEW_PATHS" | { - case "$NIX_CI_CACHE_STRATEGY" in - "runner") - export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} - # add ^* to all store paths ending in .drv (prevent warning log spam) - sed '/\.drv$/s/$/^*/' | nix copy --quiet --to "$RUNNER_CACHE" --stdin || true - ;; - "attic") - attic push --stdin ci:$ATTIC_CACHE || true - ;; - "cachix") - cachix push $CACHIX_CACHE || true - ;; - "none") - echo "Cache strategy is none, doing nothing..." - ;; - *) - echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" - ;; - esac - } - echo -e "\\e[0Ksection_end:`date +%s`:cache_push\\r\\e[0K" - else - echo "Caching disabled, not uploading $COUNT new store entries..." - fi -echo -e "\\e[0Ksection_end:`date +%s`:finalize_nix_ci\\r\\e[0K" - diff --git a/scripts/setup_nix_ci.sh b/scripts/setup_nix_ci.sh deleted file mode 100644 index 1f2af66..0000000 --- a/scripts/setup_nix_ci.sh +++ /dev/null @@ -1,49 +0,0 @@ -echo -e "\\e[0Ksection_start:`date +%s`:nix_setup[collapsed=true]\\r\\e[0KSetting up Nix CI" - nix path-info --all > /tmp/nix-store-before - - if [[ "$NIX_CI_CACHE_STRATEGY" == "auto" ]]; then - export NIX_CI_CACHE_STRATEGY="${NIX_CI_RUNNER_CACHE_STRATEGY:-${NIX_CI_DEFAULT_CACHE_STRATEGY:-none}}"; - echo "NIX_CI_CACHE_STRATEGY was set to auto, selected '$NIX_CI_CACHE_STRATEGY' for this job" - fi - - if [ -z "$NIX_CI_DISABLE_CACHE" ]; then - echo -e "\\e[0Ksection_start:`date +%s`:cache_setup[collapsed=true]\\r\\e[0KConfiguring cache ($NIX_CI_CACHE_STRATEGY)" - case "$NIX_CI_CACHE_STRATEGY" in - "runner") - export RUNNER_CACHE=''${RUNNER_CACHE:-"file://$(pwd)/.nix-cache"} - echo "Runner Cache: $RUNNER_CACHE" - export NIX_CONFIG="$NIX_CONFIG - extra-trusted-substituters = $RUNNER_CACHE?priority=10&trusted=true - extra-substituters = $RUNNER_CACHE?priority=10&trusted=true - " - ;; - "attic") - echo "Attic Cache: $ATTIC_CACHE" - attic login --set-default ci "$ATTIC_SERVER" "$ATTIC_TOKEN" || true - attic use "$ATTIC_CACHE" || true - ;; - "cachix") - echo "Cachix Cache: $CACHIX_CACHE" - cachix use "$CACHIX_CACHE" || true - ;; - "none") - echo "Cache strategy is none, doing nothing..." - ;; - *) - echo "WARNING: Invalid cache strategy set: '$NIX_CI_CACHE_STRATEGY'" - ;; - esac - echo -e "\\e[0Ksection_end:`date +%s`:cache_setup\\r\\e[0K" - else - echo "Caching disabled (NIX_CI_DISABLE_CACHE), skipping cache configuration..." - fi - - # load the job's deps only if the name was passed - if [[ ! -z $1 ]]; then - echo -e "\\e[0Ksection_start:`date +%s`:nix_deps[collapsed=true]\\r\\e[0KFetching Nix dependencies for job" - nix build .#$1 - source $(readlink -f result) - echo -e "\\e[0Ksection_end:`date +%s`:nix_deps\\r\\e[0K" - fi -echo -e "\\e[0Ksection_end:`date +%s`:nix_setup\\r\\e[0K" - diff --git a/snapshots/default.snap.json b/snapshots/default.snap.json deleted file mode 100644 index c247a78..0000000 --- a/snapshots/default.snap.json +++ /dev/null @@ -1 +0,0 @@ -{"docs":{"after_script":["finalize_nix_ci"],"artifacts":{"paths":["public"]},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:docs\""],"image":"$NIX_CI_IMAGE","script":["nix build .#docs:default\nmkdir -p public\ncp -r result/. public/\n"],"stage":"build"},"nixtest":{"after_script":["finalize_nix_ci"],"allow_failure":true,"artifacts":{"reports":{"junit":"junit.xml"},"when":"always"},"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:nixtest\""],"image":"$NIX_CI_IMAGE","script":["nix run .#nixtests:run -- --junit=junit.xml --pure"],"stage":"nixtest"},"pages":{"artifacts":{"paths":["public"]},"image":"alpine:latest","rules":[{"if":"$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"}],"script":["true"],"stage":"deploy"},"stages":["test","nixtest","build","deploy"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test\""],"cache":[{"key":"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG","paths":[".nix-cache/"]}],"image":"$NIX_CI_IMAGE","script":["hello","curl google.de","echo $TEST $TEST_WITH_DERIVATION"],"stage":"test","variables":{"NIX_CI_CACHE_STRATEGY":"runner","TEST":"test"}},"test-default":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:default:job-deps:test-default\""],"image":"$NIX_CI_IMAGE","script":["hello"],"stage":"test"},"test-non-nix":{"image":"alpine:latest","script":["echo \"This job will not be modified to use nix\""],"stage":"test"}} \ No newline at end of file diff --git a/snapshots/non-default.snap.json b/snapshots/non-default.snap.json deleted file mode 100644 index 9ed3256..0000000 --- a/snapshots/non-default.snap.json +++ /dev/null @@ -1 +0,0 @@ -{"stages":["test"],"test":{"after_script":["finalize_nix_ci"],"before_script":["source setup_nix_ci \"gitlab-ci:pipeline:non-default:job-deps:test\""],"image":"$NIX_CI_IMAGE","script":["echo Hello from another pipeline"],"stage":"test"}} \ No newline at end of file diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml deleted file mode 100644 index 2dcead3..0000000 --- a/templates/nix-gitlab-ci.yml +++ /dev/null @@ -1,112 +0,0 @@ -spec: - inputs: - cache_strategy: - type: string - description: | - auto (default) | none | runner | cachix | attic - Sets the default caching strategy. - - "auto": dynamically selects the best strategy for every job based on env variables - - "none": disables caching - - "runner", "cachix" & "attic": forces every job to use this strategy - - Can be overridden by setting NIX_CI_CACHE_STRATEGY in the pipeline variables. - default: "auto" - cache_files: - type: array - description: | - Files to use as the cache key for the generated pipeline yaml. - If you use "ci.nix" to define CI, add that here for example. - Note that max 2 items are allowed in cache:key:files, so use something like - ["flake.*", "ci.nix"] f. ex. to match flake.lock, flake.nix and ci.nix. - default: ["flake.nix", "flake.lock"] - version: - type: string - description: | - Which version of the Nix CI image to use. Using a tag/version is recommended. - stage_build: - type: string - description: The CI stage for building the dynamic pipeline. - default: build - stage_trigger: - type: string - description: The CI stage for triggering the dynamic pipeline. - default: trigger ---- -stages: - - $[[ inputs.stage_build ]] - - $[[ inputs.stage_trigger ]] -variables: - # These can be overriden, see https://docs.gitlab.com/ci/variables/#cicd-variable-precedence - # which image should be used by default. - NIX_CI_IMAGE: registry.gitlab.com/technofab/nix-gitlab-ci/nix-ci:$[[ inputs.version ]] - # default cache stategy - NIX_CI_CACHE_STRATEGY: $[[ inputs.cache_strategy ]] -nix-ci:build: - stage: $[[ inputs.stage_build ]] - image: $NIX_CI_IMAGE - cache: - - key: - files: $[[ inputs.cache_files ]] - paths: - - .nix-ci-pipelines/ - - key: nix - paths: - - .nix-cache/ - before_script: - - | - # if no explicit pipeline is requested - if [[ -z "${NIX_CI_PIPELINE_NAME:-}" ]]; then - # if regex matches, use pipeline "default", otherwise $CI_PIPELINE_SOURCE - [[ "${CI_PIPELINE_SOURCE}" =~ ${NIX_CI_DEFAULT_SOURCES:-.*} ]] \ - && NIX_CI_PIPELINE_NAME="default" \ - || NIX_CI_PIPELINE_NAME="$CI_PIPELINE_SOURCE"; - fi - echo "NIX_CI_GENERATED_PIPELINE_NAME=$NIX_CI_PIPELINE_NAME" >> trigger.env - # inheritance of pipeline variables is a bit weird, so explicitly override them - # (ctx: setting any of these in the project variables would only apply correctly - # in this pipeline, not the child pipeline, instead weirdly enough the default - # variables above are used). If any other variables are added at the top, add them - # here aswell - echo "NIX_CI_IMAGE=$NIX_CI_IMAGE" >> trigger.env - echo "NIX_CI_CACHE_STRATEGY=$NIX_CI_CACHE_STRATEGY" >> trigger.env - - mkdir -p .nix-ci-pipelines/ - # generated-gitlab-ci.yml exists in the cache - [[ -f ".nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml" ]] && export CACHED=true && echo "A cached pipeline file exists (skip cache with NIX_CI_FORCE_BUILD)" || true - # allow the user to manually skip the cache (when the key files are not correctly configured etc.) - [[ -n "$NIX_CI_FORCE_BUILD" ]] && unset CACHED && echo "Caching skipped for this job (through NIX_CI_FORCE_BUILD)" || true - - # only setup when we need to generate the pipeline yaml - if [[ -z "$CACHED" ]]; then - source setup_nix_ci; - fi - script: - # build the pipeline if it does not exist in the cache - - > - if [[ -z "$CACHED" ]]; then - nix build .#gitlab-ci:pipeline:${NIX_CI_PIPELINE_NAME} && install result .nix-ci-pipelines/${NIX_CI_PIPELINE_NAME}.yml; - fi - after_script: - # save to binary cache or Gitlab CI cache only if we actually built something - # check if /tmp/nix-store-before exists as $CACHED never exists here and the file only exists if "setup_nix_ci" is called - - | - if [[ -f "/tmp/nix-store-before" ]]; then - finalize_nix_ci; - fi - artifacts: - paths: - - .nix-ci-pipelines/ - reports: - dotenv: trigger.env - -nix-ci:trigger: - stage: $[[ inputs.stage_trigger ]] - needs: - - nix-ci:build - trigger: - include: - - artifact: .nix-ci-pipelines/${NIX_CI_GENERATED_PIPELINE_NAME}.yml - job: nix-ci:build - strategy: depend - forward: - pipeline_variables: true diff --git a/tests/ci-lib.nix b/tests/ci-lib.nix deleted file mode 100644 index 577139c..0000000 --- a/tests/ci-lib.nix +++ /dev/null @@ -1,140 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./../lib {inherit lib pkgs;}; -in { - nixtest.suites."CI Lib" = { - pos = __curPos; - tests = let - inherit (cilib.jobDeps) mkJobDeps; - inherit (cilib.jobRun) mkJobRun; - inherit (cilib.jobPatch) mkJobPatched; - inherit (cilib.pipeline) mkPipeline; - deps = mkJobDeps { - key = "test"; - job = { - nix.deps = [pkgs.hello]; - variables.TEST = "${pkgs.curl}"; - }; - }; - in [ - { - name = "jobDeps"; - type = "script"; - script = - # sh - '' - export PATH=${lib.makeBinPath [pkgs.gnugrep]} - grep -q "/nix/store" ${deps} - grep -q 'hello/bin:$PATH' ${deps} - grep -q "export TEST=" ${deps} - grep -q "curl" ${deps} - ''; - } - { - name = "jobRun"; - type = "script"; - script = let - run = mkJobRun { - key = "test"; - job.script = ["hello"]; - jobDeps = deps; - }; - in - # sh - '' - export PATH=${lib.makeBinPath [pkgs.gnugrep]} - grep -q "sandbox-helper" ${run}/bin/gitlab-ci-job:test - grep -q "gitlab-ci-job-test-raw" ${run}/bin/gitlab-ci-job:test - grep -q "gitlab-ci-job-deps-test" ${run.passthru.actualJobScript} - grep -q "Running script..." ${run.passthru.actualJobScript} - grep -q "hello" ${run.passthru.actualJobScript} - ''; - } - { - name = "jobPatched nix disabled"; - expected = {}; - actual = mkJobPatched { - key = "test"; - pipeline_name = "test"; - job.nix.enable = false; - }; - } - { - name = "jobPatched without runner cache"; - expected = { - after_script = ["finalize_nix_ci"]; - before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; - }; - actual = mkJobPatched { - key = "test"; - pipeline_name = "test"; - job.nix = { - enable = true; - enable-runner-cache = false; - }; - }; - } - { - name = "jobPatched with runner cache"; - expected = { - after_script = ["finalize_nix_ci"]; - before_script = ["source setup_nix_ci \"gitlab-ci:pipeline:test:job-deps:test\""]; - cache = [ - { - key = "test"; - paths = [".nix-cache/"]; - } - ]; - variables."NIX_CI_CACHE_STRATEGY" = "runner"; - }; - actual = mkJobPatched { - key = "test"; - pipeline_name = "test"; - job.nix = { - enable = true; - enable-runner-cache = true; - runner-cache-key = "test"; - }; - }; - } - { - name = "mkPipeline empty"; - expected = {}; - actual = - (mkPipeline { - name = "test"; - pipeline.jobs = {}; - }).finalConfig; - } - { - name = "mkPipeline empty packages"; - type = "script"; - script = let - pipeline = builtins.toFile "pipeline-test" (builtins.toJSON - (mkPipeline { - name = "test"; - pipeline.jobs = {}; - }).packages); - in - # sh - '' - set -euo pipefail - export PATH=${lib.makeBinPath [pkgs.jq pkgs.gnugrep pkgs.coreutils]} - # single key - jq 'keys | length == 1' "${pipeline}" | grep -q true - # key is exactly "gitlab-ci:pipeline:test" - jq -r 'keys[0]' "${pipeline}" | grep -qx "gitlab-ci:pipeline:test" - # value contains "/nix/store/" - jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "/nix/store/" - # value contains "gitlab-ci-test.yml" - jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}" | grep -q "gitlab-ci-test.yml" - # file only contains "{}" - [[ "$(cat $(jq -r '.["gitlab-ci:pipeline:test"]' "${pipeline}"))" == "{}" ]] - ''; - } - ]; - }; -} diff --git a/tests/default.nix b/tests/default.nix deleted file mode 100644 index 764ae46..0000000 --- a/tests/default.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ - imports = [ - ./utils.nix - ./ci-lib.nix - ./helpers.nix - ./pipeline-yamls.nix - ]; -} diff --git a/tests/helpers.nix b/tests/helpers.nix deleted file mode 100644 index 1d100fc..0000000 --- a/tests/helpers.nix +++ /dev/null @@ -1,96 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./../lib {inherit lib pkgs;}; -in { - nixtest.suites."Helpers" = { - pos = __curPos; - tests = let - inherit (cilib) helpers; - in [ - { - name = "appendToAfterScript nix disabled"; - expected = {}; - actual = helpers.appendToAfterScript [] {}; - } - { - name = "appendToAfterScript empty"; - expected = { - nix.enable = true; - after_script = []; - }; - actual = helpers.appendToAfterScript [] {nix.enable = true;}; - } - { - name = "appendToAfterScript"; - expected = { - nix.enable = true; - after_script = ["echo after_script" "finalize_nix_ci"]; - }; - actual = helpers.appendToAfterScript ["finalize_nix_ci"] { - nix.enable = true; - after_script = ["echo after_script"]; - }; - } - { - name = "prependToBeforeScript nix disabled"; - expected = {}; - actual = helpers.prependToBeforeScript [] {}; - } - { - name = "prependToBeforeScript empty"; - expected = { - nix.enable = true; - before_script = []; - }; - actual = helpers.prependToBeforeScript [] {nix.enable = true;}; - } - { - name = "prependToBeforeScript"; - expected = { - nix.enable = true; - before_script = ["setup_nix_ci" "echo before_script"]; - }; - actual = helpers.prependToBeforeScript ["setup_nix_ci"] { - nix.enable = true; - before_script = ["echo before_script"]; - }; - } - { - name = "toYaml"; - expected = ''{"hello":"world"}''; - actual = builtins.readFile (helpers.toYaml "test" {hello = "world";}); - } - { - name = "filterAttrsRec"; - expected = {world = "world";}; - actual = helpers.filterAttrsRec (n: v: v != null) { - hello = null; - world = "world"; - }; - } - { - name = "filterJobVariables with store paths"; - expected = {HELLO = "${pkgs.hello}";}; - actual = helpers.filterJobVariables true { - variables = { - HELLO = "${pkgs.hello}"; - WORLD = "world"; - }; - }; - } - { - name = "filterJobVariables without store paths"; - expected = {WORLD = "world";}; - actual = helpers.filterJobVariables false { - variables = { - HELLO = "${pkgs.hello}"; - WORLD = "world"; - }; - }; - } - ]; - }; -} diff --git a/tests/pipeline-yamls.nix b/tests/pipeline-yamls.nix deleted file mode 100644 index 32effbe..0000000 --- a/tests/pipeline-yamls.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - lib, - pkgs, - self', - ... -}: let - cilib = import ./../lib {inherit lib pkgs;}; -in { - nixtest.suites."Pipeline YAMLs" = { - pos = __curPos; - tests = let - jsonFile = file: builtins.fromJSON (builtins.readFile file); - in [ - { - name = "default"; - type = "snapshot"; - actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:default"; - } - { - name = "non-default"; - type = "snapshot"; - actual = jsonFile self'.legacyPackages."gitlab-ci:pipeline:non-default"; - } - ]; - }; -} diff --git a/tests/utils.nix b/tests/utils.nix deleted file mode 100644 index 8d9591f..0000000 --- a/tests/utils.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - cilib = import ./../lib {inherit lib pkgs;}; -in { - nixtest.suites."Utils" = { - pos = __curPos; - tests = [ - { - name = "commitAndPushFiles"; - type = "script"; - script = let - inherit (cilib) utils; - job = builtins.toFile "test" ( - builtins.unsafeDiscardStringContext ( - builtins.toJSON ( - utils.commitAndPushFiles { - message = "hello world"; - files = ["a.md" "b.txt"]; - } {} - ) - ) - ); - in - # sh - '' - export PATH=${lib.makeBinPath [pkgs.gnugrep]} - grep -q 'git commit -m \\"hello world\\"' ${job} - grep -q 'git add a.md b.txt' ${job} - ''; - } - ]; - }; -} From 7cb73d23190502a7e7a124c4c73bcedf1d45398a Mon Sep 17 00:00:00 2001 From: technofab Date: Mon, 6 Oct 2025 17:56:57 +0200 Subject: [PATCH 087/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index 0c05817..4a36342 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.0-alpha.3 +3.0.0 From fa33f6e0b71d0a2d733c5ad08c2ea9a99a2b7374 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 9 Oct 2025 14:03:39 +0200 Subject: [PATCH 088/102] chore: alias original pipeline source & document component issue Closes #27 --- docs/cicd_component.md | 7 +++++++ templates/nix-gitlab-ci.yml | 1 + 2 files changed, 8 insertions(+) diff --git a/docs/cicd_component.md b/docs/cicd_component.md index 2fe2ae2..4ea3b17 100644 --- a/docs/cicd_component.md +++ b/docs/cicd_component.md @@ -2,6 +2,13 @@ The CI/CD Component has some inputs which configure defaults for Nix GitLab CI. +!!! WARNING + + If you get errors like `the component path is not supported` it might be related to + [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/437996#note_1775337668). + + See [here](https://gitlab.com/TECHNOFAB/nix-gitlab-ci/-/issues/27) for more. + ## `version` - Type: `string` diff --git a/templates/nix-gitlab-ci.yml b/templates/nix-gitlab-ci.yml index 14eeaa9..57c56d9 100644 --- a/templates/nix-gitlab-ci.yml +++ b/templates/nix-gitlab-ci.yml @@ -62,6 +62,7 @@ nix-ci:build: || NIX_CI_PIPELINE_NAME="$CI_PIPELINE_SOURCE"; fi echo "NIX_CI_GENERATED_PIPELINE_NAME=$NIX_CI_PIPELINE_NAME" >> trigger.env + echo "ORIGINAL_CI_PIPELINE_SOURCE=$CI_PIPELINE_SOURCE" >> trigger.env # inheritance of pipeline variables is a bit weird, so explicitly override them # (ctx: setting any of these in the project variables would only apply correctly # in this pipeline, not the child pipeline, instead weirdly enough the default From afe1e02310de93395e8d6a94d17acb0c60f85b42 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 13 Nov 2025 21:39:38 +0100 Subject: [PATCH 089/102] fix(job): use unmodified job for mkJobRun otherwise it tries to run "setup_nix_ci" etc. when running locally, which doesn't make sense here --- lib/impl/modules/job.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index 5f7d1ee..295093d 100644 --- a/lib/impl/modules/job.nix +++ b/lib/impl/modules/job.nix @@ -637,12 +637,12 @@ in rec { // gitlabOptions; config = let attrsToKeep = builtins.attrNames gitlabOptions; + job = filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config); in { finalConfig = cilib.mkJobPatched { key = name; - job = filterUnset (filterAttrs (n: _v: builtins.elem n attrsToKeep) config); nixConfig = config.nix; - inherit pipelineName; + inherit job pipelineName; }; depsDrv = cilib.mkJobDeps { key = name; @@ -651,8 +651,8 @@ in rec { }; runnerDrv = cilib.mkJobRun { key = name; - job = config.finalConfig; jobDeps = config.depsDrv; + inherit job; }; packages = { "gitlab-ci:pipeline:${pipelineName}:job-deps:${name}" = config.depsDrv; From f5181b7b611204353b688cb557ade99426d2b01f Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 13 Nov 2025 21:40:24 +0100 Subject: [PATCH 090/102] fix(sandbox_helper): fix comparisons, rename TMPDIR variable, add help 1. fixes comparisons with true for flag variables 2. renames TMPDIR to NGCI_TMPDIR so it doesn't interfere with the standardized TMPDIR var (and at some point accidentally deleting /tmp) 3. add small help message when invalid arg/param is passed 4. run `git add .` on copied git repo in /tmp so staged files stay staged there aswell --- lib/impl/sandbox_helper.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/impl/sandbox_helper.sh b/lib/impl/sandbox_helper.sh index 294fc6b..8b76ae1 100644 --- a/lib/impl/sandbox_helper.sh +++ b/lib/impl/sandbox_helper.sh @@ -28,18 +28,19 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown option: $1" >&2 + echo "use --include-dirty, --no-sandbox, --keep-tmp and --keep-env " >&2 exit 1 ;; esac done -if [ "$NO_SANDBOX" = false ]; then +if [ $NO_SANDBOX = false ]; then echo "Running with simple sandboxing" - TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") - if [ "$KEEP_TMP" = false ]; then - trap "rm -rf '$TMPDIR'" EXIT + NGCI_TMPDIR=$(mktemp -dt "nix-gitlab-ci.XXX") + if [ $KEEP_TMP = false ]; then + trap "rm -rf '$NGCI_TMPDIR'" EXIT else - echo "Temp dir will be preserved at: $TMPDIR" + echo "Temp dir will be preserved at: $NGCI_TMPDIR" fi # check if dirty @@ -50,14 +51,15 @@ if [ "$NO_SANDBOX" = false ]; then git diff --staged > "$DIRTY_PATCH" trap "rm -f '$DIRTY_PATCH'" EXIT fi - git clone . $TMPDIR - pushd $TMPDIR >/dev/null - if [[ ! -z "$DIRTY_PATCH" && "$INCLUDE_DIRTY" = true ]]; then + git clone . $NGCI_TMPDIR + pushd $NGCI_TMPDIR >/dev/null + if [[ ! -z "$DIRTY_PATCH" && $INCLUDE_DIRTY = true ]]; then echo "Copying dirty changes..." git apply "$DIRTY_PATCH" 2>/dev/null || echo "Failed to copy dirty changes" + git add . # required so the files are staged again fi - echo "Running job in $TMPDIR" + echo "Running job in $NGCI_TMPDIR" env -i $( if [[ -n "$KEEP_ENV" ]]; then IFS=',' read -ra VARS <<< "$KEEP_ENV" From 524bdf9cdcfb8008c08d7e54a95992ebf05331d5 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 13 Nov 2025 21:44:28 +0100 Subject: [PATCH 091/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index 4a36342..cb2b00e 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.0 +3.0.1 From d0662e31851ccb33d38a82938f2042fe013ecc33 Mon Sep 17 00:00:00 2001 From: Skryta Istota <6970043-hidden-being@users.noreply.gitlab.com> Date: Sun, 30 Nov 2025 11:44:23 +0100 Subject: [PATCH 092/102] fix(helpers): use builtin nix store location indicator --- lib/impl/helpers.nix | 2 +- tests/cilib_test.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 48a79fc..754cead 100644 --- a/lib/impl/helpers.nix +++ b/lib/impl/helpers.nix @@ -50,7 +50,7 @@ in rec { filterJobVariables = shouldContain: job: concatMapAttrs ( name: value: - optionalAttrs ((hasInfix "/nix/store/" value) == shouldContain) { + optionalAttrs ((hasInfix builtins.storeDir value) == shouldContain) { ${name} = value; } ) diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix index 78e99a3..b53308c 100644 --- a/tests/cilib_test.nix +++ b/tests/cilib_test.nix @@ -129,7 +129,7 @@ # sh '' set -euo pipefail - ${ntlib.helpers.path [pkgs.jq pkgs.gnugrep pkgs.coreutils]} + ${ntlib.helpers.path (with pkgs; [jq gnugrep coreutils])} echo "two keys, one json one pretty" jq 'keys | length == 2' "${pipeline}" | grep -q true echo "key[0] is exactly 'gitlab-ci:pipeline:test'" From 96e6fe59bffec37e89ef38d78d50c0a3de43ea44 Mon Sep 17 00:00:00 2001 From: Skryta Istota <6970043-hidden-being@users.noreply.gitlab.com> Date: Sun, 30 Nov 2025 14:14:14 +0100 Subject: [PATCH 093/102] ci: fix oci image used for dog fooding & forks --- .gitlab-ci.yml | 2 ++ nix/repo/ci.nix | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bff742c..e8c146c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,3 +55,5 @@ stages: - build-images - build - trigger +variables: + NIX_CI_IMAGE: $CI_REGISTRY_IMAGE/nix-ci:$CI_COMMIT_SHORT_SHA diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index a76c0d8..157ede7 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -10,6 +10,7 @@ in # the child pipeline can then use the built images to test them extraData = { stages = ["build-images" "build" "trigger"]; + variables.NIX_CI_IMAGE = "$CI_REGISTRY_IMAGE/nix-ci:$CI_COMMIT_SHORT_SHA"; "build:image" = { stage = "build-images"; parallel.matrix = [ From 1c9e7c77c5f17c6f15bc6b0f705b551ac5b70e2b Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Dec 2025 15:09:27 +0100 Subject: [PATCH 094/102] chore: add test and docs for handling nix store paths in global variables --- docs/index.md | 11 +++++++++++ tests/cilib_test.nix | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/docs/index.md b/docs/index.md index 214fbf3..14f7da3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,3 +9,14 @@ This project provides a Nix flake module that allows you to generate your `.gitl - **Modularity:** Define and manage your CI configurations in a structured and modular way using Nix modules, making it easier to share and reuse CI logic across multiple projects. This documentation will guide you through setting up and using Nix GitLab CI for your projects. + +## Warnings + +To save you from frantically searching these docs if something doesn't work as expected, here are the most important warnings ;) + +!!! warning + + Do not put Nix store paths into global/pipeline variables. They will simply be passed through, + resulting in bad portability (if two runners have different archs for example, one cannot find the path). + If you need any Nix store path in env variables, always do it on the job level, there + it will automatically be computed at runtime, thus will always work no matter which runner it runs on. diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix index b53308c..2322906 100644 --- a/tests/cilib_test.nix +++ b/tests/cilib_test.nix @@ -163,6 +163,28 @@ }; }).finalConfig; } + { + # it doesn't make much sense to have any nix store path in variables, but we ignore it for global variables + name = "ignore store paths in global variables"; + expected = { + variables = { + HELLO = "world"; + CURL = toString pkgs.curl; + }; + }; + actual = + (mkPipeline { + name = "test"; + nixConfig.enable = true; + pipeline = { + variables = { + HELLO = "world"; + CURL = toString pkgs.curl; + }; + jobs = {}; + }; + }).finalConfig; + } ]; }; } From 97fb4fafc3edb77b2bbfcf264be40e5b30b058af Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Dec 2025 15:10:00 +0100 Subject: [PATCH 095/102] fix(jobPatched): handle non-nix jobs correctly fix mkJobPatched removing `cache` and `variables` from non-nix jobs See !15 for more --- lib/impl/jobPatched.nix | 13 ++++++------- tests/cilib_test.nix | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/impl/jobPatched.nix b/lib/impl/jobPatched.nix index c7134fb..367a374 100644 --- a/lib/impl/jobPatched.nix +++ b/lib/impl/jobPatched.nix @@ -11,13 +11,13 @@ in pipelineName, nixConfig, }: - (builtins.removeAttrs job ["variables" "cache"]) - // (optionalAttrs nixConfig.enable ( - (prependToBeforeScript ["source setup_nix_ci \"gitlab-ci:pipeline:${pipelineName}:job-deps:${key}\""] job) + if ! nixConfig.enable + then job + else + (builtins.removeAttrs job ["variables" "cache"]) + // (prependToBeforeScript ["source setup_nix_ci \"gitlab-ci:pipeline:${pipelineName}:job-deps:${key}\""] job) // (appendToAfterScript ["finalize_nix_ci"] job) - )) - // optionalAttrs nixConfig.enable ( - (let + // (let variables = (filterJobVariables false job) // optionalAttrs nixConfig.enableRunnerCache { @@ -40,4 +40,3 @@ in optionalAttrs (cache != []) { inherit cache; }) - ) diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix index 2322906..3b76fb8 100644 --- a/tests/cilib_test.nix +++ b/tests/cilib_test.nix @@ -64,6 +64,22 @@ nixConfig.enable = false; }; } + { + name = "jobPatched nix disabled with variables and cache"; + expected = { + variables."HELLO" = "world"; + cache = [{key = "example";}]; + }; + actual = mkJobPatched { + key = "test"; + pipelineName = "test"; + job = { + variables."HELLO" = "world"; + cache = [{key = "example";}]; + }; + nixConfig.enable = false; + }; + } { name = "jobPatched without runner cache"; expected = { @@ -145,10 +161,13 @@ ''; } { - name = "handle store paths in variables"; + name = "ignore store paths in variables with nix disabled"; expected = { stages = ["test"]; - test.stage = "test"; + test = { + stage = "test"; + variables."TEST" = "${pkgs.hello}"; + }; }; actual = (mkPipeline { From 59f8bd169ae2733238455899fb3bace9cab99ff7 Mon Sep 17 00:00:00 2001 From: technofab Date: Tue, 2 Dec 2025 15:12:28 +0100 Subject: [PATCH 096/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index cb2b00e..fd2a018 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.1 +3.1.0 From 1e9ddff3003c1a1a349bc74b95ad222b288e3f3b Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Dec 2025 20:47:39 +0100 Subject: [PATCH 097/102] fix(modules/job): fix variables with nix store paths getting dropped --- lib/impl/modules/job.nix | 2 +- tests/modules_test.nix | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index 295093d..fd4fc62 100644 --- a/lib/impl/modules/job.nix +++ b/lib/impl/modules/job.nix @@ -646,8 +646,8 @@ in rec { }; depsDrv = cilib.mkJobDeps { key = name; - job = config.finalConfig; nixConfig = config.nix; + inherit job; }; runnerDrv = cilib.mkJobRun { key = name; diff --git a/tests/modules_test.nix b/tests/modules_test.nix index e134293..8ccbd83 100644 --- a/tests/modules_test.nix +++ b/tests/modules_test.nix @@ -77,6 +77,27 @@ assert_file_contains ${package} '"EXAMPLE":"/nix/store/.*-hello-.*"' ''; } + { + name = "correctly inject variables containing nix store paths at runtime"; + type = "script"; + script = let + package = + (cilib.mkCI { + pipelines."test".jobs."test" = { + stage = ".pre"; + variables.EXAMPLE = "${pkgs.hello}"; + script = []; + }; + }).packages."gitlab-ci:pipeline:test:job-deps:test"; + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${package} 'export PATH=":$PATH";' + assert_file_contains ${package} 'export EXAMPLE="/nix/store/.*-hello-.*"' + ''; + } ]; }; } From 8a77208ebe29fe7b3ba60bec3d48868049f93013 Mon Sep 17 00:00:00 2001 From: technofab Date: Wed, 3 Dec 2025 21:02:46 +0100 Subject: [PATCH 098/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index fd2a018..94ff29c 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.1.0 +3.1.1 From 8eadfb56ba2fc2acd0689adef8f0b214f0a7c82a Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 5 Dec 2025 20:16:37 +0100 Subject: [PATCH 099/102] fix(modules): fold was deprecated, replace with foldr --- lib/impl/modules/root.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/impl/modules/root.nix b/lib/impl/modules/root.nix index 84fb5ba..bd99050 100644 --- a/lib/impl/modules/root.nix +++ b/lib/impl/modules/root.nix @@ -4,7 +4,7 @@ pipelineSubmodule, ... }: let - inherit (lib) mkOption types; + inherit (lib) mkOption types foldr; in rec { configSubmodule = { options = { @@ -65,7 +65,7 @@ in rec { }; }; config = { - packages = lib.fold (pipeline: acc: acc // pipeline) {} ( + packages = foldr (pipeline: acc: acc // pipeline) {} ( map (pipeline: pipeline.packages) (builtins.attrValues config.pipelines) ); soonix = config.config.soonix.finalConfig; From 555ae3de29c5edfaf2b11e122d0309d1f201fe62 Mon Sep 17 00:00:00 2001 From: technofab Date: Fri, 5 Dec 2025 20:21:26 +0100 Subject: [PATCH 100/102] chore: update flakes --- flake.lock | 12 ++++++------ nix/repo/flake.lock | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/flake.lock b/flake.lock index 7c47f5c..60df9c4 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1756542300, - "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", + "lastModified": 1764667669, + "narHash": "sha256-7WUCZfmqLAssbDqwg9cUDAXrSoXN79eEEq17qhTNM/Y=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", + "rev": "418468ac9527e799809c900eda37cbff999199b6", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "locked": { "dir": "lib", - "lastModified": 1756370106, - "narHash": "sha256-l84ojcHuQWBwn4BRxQsMMfQpcq/Az/sHh/hSqFgVtyg=", + "lastModified": 1758738378, + "narHash": "sha256-NjzqdvQCDDdObEBH8x/vdhbdhrIB+N9E570uCdksGHY=", "owner": "rensa-nix", "repo": "core", - "rev": "9c1a29fa9ba7cbbb78b9e47eb8afbcd29303a3b4", + "rev": "abe19f9f13aff41de2b63304545c87d193d19ef4", "type": "gitlab" }, "original": { diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index 0627f8b..e5029e0 100644 --- a/nix/repo/flake.lock +++ b/nix/repo/flake.lock @@ -3,11 +3,11 @@ "devshell-lib": { "locked": { "dir": "lib", - "lastModified": 1755673398, - "narHash": "sha256-51MmR+Eo1+bKDd/Ss77wwTqi4yAR2xgmyCSEbKWSpj0=", + "lastModified": 1758204313, + "narHash": "sha256-ainbY0Oajb1HMdvy+A8QxF/P5qwcbEzJGEY5pzKdDdc=", "owner": "rensa-nix", "repo": "devshell", - "rev": "e76bef387e8a4574f9b6d37b1a424e706491af08", + "rev": "7d0c4bc78d9f017a739b0c7eb2f4e563118353e6", "type": "gitlab" }, "original": { @@ -20,11 +20,11 @@ "nixmkdocs-lib": { "locked": { "dir": "lib", - "lastModified": 1757055638, - "narHash": "sha256-KHYSkEreFe4meXzSdEbknC/HwaQSNClQkc8vzHlAsMM=", + "lastModified": 1763481845, + "narHash": "sha256-Bp0+9rDmlPWMcnKqGx+BG4+o5KO8FuDAOvXRnXrm3Fo=", "owner": "TECHNOFAB", "repo": "nixmkdocs", - "rev": "7840a5febdbeaf2da90babf6c94b3d0929d2bf74", + "rev": "73d59093df94a894d25bc4bf71880b6f00faa62f", "type": "gitlab" }, "original": { @@ -37,11 +37,11 @@ "nixtest-lib": { "locked": { "dir": "lib", - "lastModified": 1756812148, - "narHash": "sha256-0g8KNk4zoLApA51PBHOWqPLRYpprjrQuSzNCjfBQgu8=", + "lastModified": 1759340550, + "narHash": "sha256-EH9heYb/nHHzCpUGQGqVQnuyVGQ7D6MVMgJmzNvvmJ8=", "owner": "TECHNOFAB", "repo": "nixtest", - "rev": "5741109cc9ec2b6d41b56abd3f5bc51ed7a9a228", + "rev": "5a7053afcbb211b9cf8fe87f7892bb9f6b76b678", "type": "gitlab" }, "original": { @@ -63,11 +63,11 @@ "soonix-lib": { "locked": { "dir": "lib", - "lastModified": 1756797658, - "narHash": "sha256-4rkyP4oaoqG/FFVL7W8U+8hGer4tOBPff/2SeN5tJYQ=", + "lastModified": 1763323017, + "narHash": "sha256-MJyg37d+VMfRoFiVUj16FW+zkEwQXbgK9LoFF/SHoxA=", "owner": "TECHNOFAB", "repo": "soonix", - "rev": "3baef660cf8b87391d475a0455dd66fae0e60008", + "rev": "078034b01e4eaf1f9436d46721f7cbe0d96eb8b4", "type": "gitlab" }, "original": { @@ -80,11 +80,11 @@ "treefmt-nix": { "flake": false, "locked": { - "lastModified": 1756662192, - "narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=", + "lastModified": 1762938485, + "narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4", + "rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4", "type": "github" }, "original": { From 8f88a53b5479773cd626420362631bc1da99e677 Mon Sep 17 00:00:00 2001 From: technofab Date: Thu, 11 Dec 2025 10:17:52 +0100 Subject: [PATCH 101/102] chore: bump version --- lib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VERSION b/lib/VERSION index 94ff29c..ef538c2 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.1.1 +3.1.2 From 139912d9c6147f6309c253126c44e8a5fb02292f Mon Sep 17 00:00:00 2001 From: asimon Date: Tue, 16 Dec 2025 19:12:06 +0100 Subject: [PATCH 102/102] docs: fix Soonix URL in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1570632..c5e6b92 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,4 @@ There is also `.#gitlab-ci:pipeline::job-deps:` which gener Some parts of this implementation are adapted/inspired from https://gitlab.com/Cynerd/gitlab-ci-nix -[docs-soonix]: https://nix-gitlab-ci.projects,tf/soonix "Soonix Integration" +[docs-soonix]: https://nix-gitlab-ci.projects.tf/soonix "Soonix Integration"