/ nix / ci.nix
ci.nix
  1  { pkgs, tests, binaries, ... }:
  2  /* CI/CD related scripts and pipelines.
  3  
  4    This file is used to dynamically generate the Buildkite pipeline (see `pipeline` down below).
  5  
  6    Arguments:
  7  
  8     - `tests`: the tests derivation.
  9     - `binaries`: a list of binaries in the crate. This is generated from the `crates` definition in flake.nix.
 10   */
 11  let
 12    /* Run all unit tests.
 13    */
 14    ci-test = pkgs.writeShellApplication {
 15      name = "ci-test";
 16      runtimeInputs = [ tests ];
 17      text = ''
 18        echo "--- Running unit tests"
 19        for testBin in ${tests}/bin/*; do
 20          echo "Running ''${testBin}"
 21          ''${testBin}
 22        done
 23      '';
 24    };
 25  
 26    /* Run all integration tests.
 27  
 28      This tests take longer to run so they're run separately.
 29    */
 30    ci-e2e-test = pkgs.writeShellApplication {
 31      name = "ci-e2e-test";
 32      runtimeInputs = [ tests ];
 33      text = ''
 34        echo "--- Running e2e tests"
 35        for testBin in ${tests}/bin/*; do
 36          echo "Running ''${testBin}"
 37          ''${testBin} --ignored
 38        done
 39      '';
 40    };
 41  
 42    /* Prepares the image to be uploaded to Buildkite.
 43  
 44      Notice that this script expects the image to be in `./result`.
 45    */
 46    ci-prepare-image = pkgs.writeShellApplication {
 47      name = "ci-prepare-image";
 48      runtimeInputs = with pkgs; [
 49        buildah
 50        skopeo
 51      ];
 52      text = ''
 53        arch=$1
 54        name=$2
 55  
 56        filename="''${name}-''${arch}-image.tar.gz"
 57        echo "--- Copying image to ''${filename}"
 58        skopeo copy "docker-archive:result" "docker-archive:''${filename}"
 59      '';
 60    };
 61  
 62  
 63    /* Prepares the binary to be uploaded to Buildkite.
 64  
 65      Notice that this script expects the binary to be in `./result/bin`.
 66    */
 67    ci-prepare-binary = pkgs.writeShellApplication {
 68      name = "ci-prepare-binary";
 69      runtimeInputs = with pkgs; [ ];
 70      text = ''
 71        os=$1
 72        arch=$2
 73        name=$3
 74  
 75        filename="''${name}-''${arch}-''${os}.gz"
 76        echo "--- Compressing binary to ''${filename}"
 77        gzip -c ./result/bin/* > "''${filename}"
 78      '';
 79    };
 80  
 81    /* Publish a container image to the registry, with support for multiple architectures.
 82  
 83      If the `BUILDKITE_TAG` env variable is not set, the script will do nothing.
 84  
 85      If the `BUILDKITE_TAG` env variable is set, the image will be versioned. The tag
 86      is expected to be in the format `$name/vX.Y.Z` where `$name` is the name of the
 87      image and `X.Y.Z` is a valid semver version.
 88      The script will then tag the image four times:
 89      - `quay.io/apibara/$name:$version`
 90      - `quay.io/apibara/$name:$major.$minor`
 91      - `quay.io/apibara/$name:$major`
 92      - `quay.io/apibara/$name:latest`
 93  
 94      Example:
 95        BUILDKITE_TAG=operator/v1.2.3
 96  
 97        The image will be tagged as:
 98         - quay.io/apibara/operator:1.2.3-x86_64
 99         - quay.io/apibara/operator:1.2.3-aarch64
100         - quay.io/apibara/operator:1.2.3
101         - quay.io/apibara/operator:1.2
102         - quay.io/apibara/operator:1
103         - quay.io/apibara/operator:latest
104  
105      Arguments:
106  
107       - `filename`: the filename of the container image to load.
108       - `name`: the name of the image to publish.
109    */
110    ci-publish-image = pkgs.writeShellApplication {
111      name = "ci-publish-image";
112      runtimeInputs = with pkgs; [
113        skopeo
114        buildah
115        semver-tool
116      ];
117      text = ''
118        function dry_run() {
119          if [[ "''${DRY_RUN:-false}" == "true" ]]; then
120            echo "[dry-run] $*"
121          else
122            "$@"
123          fi
124        }
125  
126        name=$1
127        shift
128        archs=( "$@" )
129  
130        if [ ''${#archs[@]} -eq 0 ]; then
131          echo "No architectures specified"
132          exit 1
133        fi
134  
135        if [ -z "''${BUILDKITE_TAG:-}" ]; then
136          echo "No tag specified"
137          exit 0
138        fi
139  
140        if [[ "''${BUILDKITE_TAG:-}" != ''${name}/v* ]]; then
141          echo "Tag is for different image"
142          exit 0
143        fi
144  
145        version=''${BUILDKITE_TAG#"''${name}/v"}
146        if [[ $(semver validate "''${version}") != "valid" ]]; then
147          echo "Invalid version ''${version}"
148          exit 1
149        fi
150  
151        base="quay.io/apibara/''${name}"
152        image="''${base}:''${version}"
153  
154        # First, load the images and push them to the registry
155        images=()
156        for arch in "''${archs[@]}"; do
157          echo "--- Copying image ''${arch}"
158  
159          filename="''${name}-''${arch}-image.tar.gz"
160          destImage="''${image}-''${arch}"
161  
162          echo "Copying ''${filename} to ''${destImage}"
163          dry_run skopeo copy "docker-archive:''${filename}" "docker://''${destImage}"
164  
165          images+=("''${destImage}")
166        done
167  
168        echo "--- Tagging release ''${base}:''${version}"
169        manifest="''${base}:''${version}"
170        dry_run buildah manifest create "''${manifest}" "''${images[@]}"
171  
172        # Tag and push image v X.Y.Z
173        echo "--- Pushing release ''${base}:''${version}"
174        dry_run buildah manifest push --all "''${manifest}" "docker://''${base}:''${version}"
175  
176        # Tag and push image v X.Y
177        tag="$(semver get major "''${version}").$(semver get minor "''${version}")"
178        echo "--- Pushing release ''${base}:''${tag}"
179        dry_run buildah manifest push --all "''${manifest}" "docker://''${base}:''${tag}"
180  
181        # Tag and push image v X
182        tag="$(semver get major "''${version}")"
183        echo "--- Pushing release ''${base}:''${tag}"
184        dry_run buildah manifest push --all "''${manifest}" "docker://''${base}:''${tag}"
185  
186        # Tag and push image latest
187        tag="latest"
188        echo "--- Pushing release ''${base}:''${tag}"
189        dry_run buildah manifest push --all "''${manifest}" "docker://''${base}:''${tag}"
190      '';
191    };
192  
193    /* Buildkite agents tags.
194  
195      The agents are tagged based on the OS and architecture they run on.
196      If a steps doesn't have an `agents` attribute, it will run on the default agent (which could have any architecture).
197    */
198    agents = {
199      x86_64-linux = {
200        queue = "default";
201        os = "linux";
202        arch = "x86_64";
203      };
204      aarch64-linux = {
205        queue = "aarch64-linux";
206        os = "linux";
207        arch = "aarch64";
208      };
209    };
210  
211    onAllAgents = f:
212      pkgs.lib.mapAttrsToList
213        (name: agent: f { inherit name agent; })
214        agents;
215  
216    /* Buildkite pipelines
217  
218      Instantiate a pipeline by running `nix eval --json .#pipeline.<name> | buildkite-agent pipeline upload`.
219    */
220    pipeline = {
221      default = {
222        steps = [
223          {
224            label = ":nix: Checks";
225            command = "nix flake check";
226          }
227          {
228            label = ":rust: Build tests";
229            command = "nix build .#tests";
230          }
231          {
232            label = ":books: Check documentation";
233            command = "nix develop .#ci -c vale docs/";
234          }
235          {
236            wait = { };
237          }
238          {
239            label = ":test_tube: Run unit tests";
240            commands = [
241              "nix develop .#test -c ci-test"
242            ];
243          }
244          {
245            label = ":test_tube: Run e2e tests";
246            commands = [
247              "podman system service --time=0 unix:///var/run/docker.sock &"
248              "nix develop .#test -c ci-e2e-test"
249            ];
250          }
251          {
252            wait = { };
253          }
254        ] ++ (onAllAgents ({ name, agent }:
255          {
256            label = ":rust: Build crate ${name}";
257            command = "nix build .#all-crates";
258            agents = {
259              queue = agent.queue;
260            };
261          })
262        ) ++ [
263          {
264            wait = { };
265          }
266          {
267            label = ":pipeline:";
268            command = ''
269              if [[ "''${BUILDKITE_BRANCH}" = "main" || -n "''${BUILDKITE_TAG}" ]]; then
270                nix eval --json .#pipeline.x86_64-linux.release | buildkite-agent pipeline upload --no-interpolation;
271              fi
272            '';
273          }
274        ];
275      };
276  
277      release = {
278        env = {
279          # Set to "true" to skip pushing images to the registry.
280          DRY_RUN = "false";
281        };
282        steps =
283          (onAllAgents ({ name, agent }:
284            {
285              group = ":rust: Build binaries ${name}";
286              steps = [
287                {
288                  label = ":rust: Build binary {{ matrix.binary }}";
289                  command = "nix build .#{{ matrix.binary }}";
290                  matrix = {
291                    setup = {
292                      binary = binaries;
293                    };
294                  };
295                  agents = {
296                    queue = agent.queue;
297                  };
298                }
299              ];
300            })
301          ) ++ [
302            {
303              wait = { };
304            }
305          ] ++ (onAllAgents ({ name, agent }:
306            {
307              group = ":rust: Build images ${name}";
308              steps = [
309                {
310                  label = ":rust: Build image {{ matrix.binary }}";
311                  commands = [
312                    "nix build .#{{ matrix.binary }}-image"
313                    "nix develop .#ci -c ci-prepare-image ${agent.arch} {{ matrix.binary }}"
314                    "buildkite-agent artifact upload {{ matrix.binary }}-${agent.arch}-image.tar.gz"
315                  ];
316                  matrix = {
317                    setup = {
318                      binary = binaries;
319                    };
320                  };
321                  agents = {
322                    queue = agent.queue;
323                  };
324                }
325              ];
326            })
327          ) ++ (onAllAgents ({ name, agent }:
328            {
329              group = ":rust: Build linux binaries ${name}";
330              steps = [
331                {
332                  label = ":rust: Build binary {{ matrix.binary }}";
333                  commands = [
334                    "nix build .#{{ matrix.binary }}"
335                    "nix develop .#ci -c ci-prepare-binary linux ${agent.arch} {{ matrix.binary }}"
336                    "buildkite-agent artifact upload {{ matrix.binary }}-${agent.arch}-linux.gz"
337                  ];
338                  matrix = {
339                    setup = {
340                      binary = binaries;
341                    };
342                  };
343                  agents = {
344                    queue = agent.queue;
345                  };
346                }
347              ];
348            })
349          ) ++ [
350            {
351              wait = { };
352            }
353            {
354              group = ":quay: Publish images";
355              steps = [
356                {
357                  label = ":quay: Publish image {{ matrix.binary }}";
358                  commands = (onAllAgents ({ agent, ... }:
359                    "buildkite-agent artifact download {{ matrix.binary }}-${agent.arch}-image.tar.gz ."
360                  )) ++ [
361                    (
362                      let
363                        archs = builtins.concatStringsSep " " (onAllAgents ({ agent, ... }: agent.arch));
364                      in
365                      "nix develop .#ci -c ci-publish-image {{ matrix.binary }} ${archs}"
366                    )
367                  ];
368                  matrix = {
369                    setup = {
370                      binary = binaries;
371                    };
372                  };
373                }
374              ];
375            }
376          ];
377      };
378    };
379  in
380  {
381    inherit pipeline;
382  
383    shell = {
384      ci = pkgs.mkShell {
385        buildInputs = with pkgs; [
386          ci-prepare-image
387          ci-prepare-binary
388          ci-publish-image
389  
390          vale
391        ];
392      };
393  
394      test = pkgs.mkShell {
395        buildInputs = with pkgs; [
396          # used by e2e tests to start test containers
397          docker-client
398  
399          ci-test
400          ci-e2e-test
401        ];
402      };
403    };
404  }