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 }