diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c6bc9f2..e8c146c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,10 +45,15 @@ 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: + cache_files: + - flake.* + - nix/repo/ci.nix version: $CI_COMMIT_SHORT_SHA stages: - build-images - build - trigger +variables: + NIX_CI_IMAGE: $CI_REGISTRY_IMAGE/nix-ci:$CI_COMMIT_SHORT_SHA 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" 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/docs/images/favicon.png b/docs/images/favicon.png deleted file mode 100644 index 135b030..0000000 Binary files a/docs/images/favicon.png and /dev/null differ diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 2a70bf6..0000000 Binary files a/docs/images/logo.png and /dev/null differ diff --git a/docs/images/logo.svg b/docs/images/logo.svg new file mode 100755 index 0000000..d8dce95 --- /dev/null +++ b/docs/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/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/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/lib/VERSION b/lib/VERSION index 0786c6b..ef538c2 100644 --- a/lib/VERSION +++ b/lib/VERSION @@ -1 +1 @@ -3.0.0-alpha.1 +3.1.2 diff --git a/lib/impl/helpers.nix b/lib/impl/helpers.nix index 66a42cf..754cead 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 builtins.storeDir value) == shouldContain) { ${name} = value; } ) @@ -70,7 +84,7 @@ in rec { unsetOr = typ: (types.either unsetType typ) // { - inherit (typ) description; + inherit (typ) description getSubOptions; }; mkUnsetOption = opts: mkOption (opts @@ -79,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/jobPatched.nix b/lib/impl/jobPatched.nix index 4bf0e00..367a374 100644 --- a/lib/impl/jobPatched.nix +++ b/lib/impl/jobPatched.nix @@ -11,13 +11,13 @@ in pipelineName, nixConfig, }: - job - // (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/lib/impl/modules/job.nix b/lib/impl/modules/job.nix index d8b91c0..fd4fc62 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,42 +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) ''; - type = types.str; }; - image = mkUnsetOption { + 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. ''; + default = "$NIX_CI_IMAGE"; + example = { + name = "super/sql:experimental"; + entrypoint = [""]; + pull_policy = "if-not-present"; + docker = { + platform = "arm64/v8"; + user = "dave"; + }; + kubernetes.user = "1001"; + }; + }; + "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) + ''; + }; + 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) + ''; }; - variables = mkUnsetOption { - type = types.attrsOf types.str; + 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) + ''; }; - before_script = mkUnsetOption { - type = types.listOf types.str; + 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 { @@ -140,22 +637,22 @@ 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; - job = config.finalConfig; nixConfig = config.nix; + inherit job; }; runnerDrv = cilib.mkJobRun { key = name; - job = config.finalConfig; jobDeps = config.depsDrv; + inherit job; }; packages = { "gitlab-ci:pipeline:${pipelineName}:job-deps:${name}" = config.depsDrv; 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 { 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; diff --git a/lib/impl/sandbox_helper.sh b/lib/impl/sandbox_helper.sh index 4ea5ec5..8b76ae1 100644 --- a/lib/impl/sandbox_helper.sh +++ b/lib/impl/sandbox_helper.sh @@ -28,17 +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" - 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 @@ -49,15 +51,15 @@ 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 + 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" diff --git a/nix/repo/ci.nix b/nix/repo/ci.nix index 9362a05..157ede7 100644 --- a/nix/repo/ci.nix +++ b/nix/repo/ci.nix @@ -3,12 +3,14 @@ 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"; + 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 = { 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 = [ 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";} diff --git a/nix/repo/flake.lock b/nix/repo/flake.lock index b5de491..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": 1755785622, - "narHash": "sha256-xBb9PCkszmrWSEqUiPC7oBJABm1thF572S5QHtloZ+M=", + "lastModified": 1763481845, + "narHash": "sha256-Bp0+9rDmlPWMcnKqGx+BG4+o5KO8FuDAOvXRnXrm3Fo=", "owner": "TECHNOFAB", "repo": "nixmkdocs", - "rev": "61da605a9bff12f66c4b743f43aea59ca200f533", + "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": { 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 diff --git a/tests/cilib_test.nix b/tests/cilib_test.nix index d37da49..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 = { @@ -129,7 +145,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'" @@ -145,7 +161,7 @@ ''; } { - name = "handle store paths in variables"; + name = "ignore store paths in variables with nix disabled"; expected = { stages = ["test"]; test = { @@ -166,6 +182,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; + } ]; }; } 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}/* . diff --git a/tests/modules_test.nix b/tests/modules_test.nix index 0b467f4..8ccbd83 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"]; @@ -76,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-.*"' + ''; + } ]; }; }