diff --git a/.github/generate-matrix/action.yml b/.github/generate-matrix/action.yml new file mode 100644 index 00000000..55d7c43d --- /dev/null +++ b/.github/generate-matrix/action.yml @@ -0,0 +1,45 @@ +name: Evaluate CI Matrix +description: Evaluate all packages, check cache status generate CI matrix and publish comment with the summary + +inputs: + is-initial: + description: Is this the start of the CI workflow, or the end + default: 'true' + required: true + cachix-cache: + description: The name of the cachix cache to use + required: true +outputs: + matrix: + description: 'Generated Matrix' + value: ${{ steps.generate-matrix.outputs.matrix }} + comment: + description: 'Comment' + value: ${{ steps.generate-matrix.outputs.comment }} + +runs: + using: "composite" + steps: + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v9 + with: + extra-conf: accept-flake-config = true + + - name: Generate CI Matrix + id: generate-matrix + shell: bash + env: + IS_INITIAL: ${{ inputs.is-initial }} + CACHIX_CACHE: ${{ inputs.cachix-cache }} + run: nix develop .#ci -c ./scripts/ci-matrix.sh + + - name: Upload CI Matrix + uses: actions/upload-artifact@v4 + with: + name: matrix-${{ inputs.is-initial == 'true' && 'pre' || 'post' }}.json + path: matrix-${{ inputs.is-initial == 'true' && 'pre' || 'post' }}.json + + - name: Update GitHub Comment + uses: marocchino/sticky-pull-request-comment@v2.9.0 + with: + path: comment.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eccf5d3e..9f6ce559 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,17 +20,24 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v9 + - name: Post initial package status comment + uses: marocchino/sticky-pull-request-comment@v2.9.0 + with: + recreate: true + message: | + Thanks for your Pull Request! - - id: generate-matrix - name: Generate Nix Matrix - run: | - set -Eeu - matrix="$(nix eval --json '.#lib.mkGHActionsMatrix')" - echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + This comment will be updated automatically with the status of each package. + + - name: Generate CI Matrix + id: generate-matrix + uses: ./.github/generate-matrix + with: + is-initial: 'true' + cachix-cache: ${{ vars.CACHIX_CACHE }} outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} + comment: ${{ steps.generate-matrix.outputs.comment }} build: needs: generate-matrix @@ -54,17 +61,24 @@ jobs: authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Build ${{ matrix.package }} - run: nix build -L --json --no-link '.#${{ matrix.attrPath }}' + run: nix build -L --json --no-link '.#packages.${{ matrix.attrPath }}' results: - if: ${{ always() }} runs-on: ubuntu-latest name: Final Results - needs: [build] + needs: [build, generate-matrix] steps: + - uses: actions/checkout@v4 + + - name: Generate Matrix + uses: ./.github/generate-matrix + with: + is-initial: 'false' + cachix-cache: ${{ vars.CACHIX_CACHE }} + - run: exit 1 if: >- - ${{ - contains(needs.*.result, 'failure') - || contains(needs.*.result, 'cancelled') - }} + ${{ fromJSON(needs.generate-matrix.outputs.matrix).include.length > 0 && + (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} + - run: exit 0 + if: ${{fromJSON(needs.generate-matrix.outputs.matrix).include.length == 0}} diff --git a/.gitignore b/.gitignore index fd9301c8..5882547c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # Nix build output +.result result +comment.md +matrix-pre.json +matrix-post.json # direnv temporary files .direnv/ diff --git a/lib/mk-gh-actions-matrix.nix b/lib/mk-gh-actions-matrix.nix index aef5e4bc..ff38c395 100644 --- a/lib/mk-gh-actions-matrix.nix +++ b/lib/mk-gh-actions-matrix.nix @@ -21,6 +21,18 @@ inherit (import ./build-status.nix {inherit lib;}) getBuildStatus; + allowedToFailMap = lib.pipe (mkGHActionsMatrix.include) [ + (builtins.groupBy (p: p.package)) + (builtins.mapAttrs ( + n: v: + builtins.mapAttrs ( + s: ps: + (builtins.head ps).allowedToFail + ) + (builtins.groupBy (p: p.system) v) + )) + ]; + mkGHActionsMatrix = { include = lib.pipe (builtins.attrNames nixSystemToGHPlatform) [ (builtins.concatMap diff --git a/scripts/ci-matrix.sh b/scripts/ci-matrix.sh new file mode 100755 index 00000000..4b5b399b --- /dev/null +++ b/scripts/ci-matrix.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root_dir="$(git rev-parse --show-toplevel)" + +# shellcheck source=./nix-eval-jobs.sh +source "$root_dir/scripts/nix-eval-jobs.sh" + +eval_packages_to_json() { + flake_attr_pre="${1:-packages}" + flake_attr_post="${2:-}" + + cachix_url="https://${CACHIX_CACHE}.cachix.org" + + nix eval --json .#lib.allowedToFailMap > "${result_dir}/allowed-to-fail.json" + + nix_eval_for_all_systems "$flake_attr_pre" "$flake_attr_post" \ + | cat "${result_dir}/allowed-to-fail.json" - \ + | jq -sr ' + .[0] as $allowed_to_fail + | .[1:] as $nix_eval_results + | { + "x86_64-linux": "ubuntu-latest", + "x86_64-darwin": "macos-14", + "aarch64-darwin": "macos-14" + } as $system_to_gh_platform + | $nix_eval_results + | map({ + package: .attr, + attrPath: "\(.system).\(.attr)", + allowedToFail: $allowed_to_fail[.attr][.system], + isCached, + system, + cache_url: .outputs.out + | "'"$cachix_url"'/\(match("^\/nix\/store\/([^-]+)-").captures[0].string).narinfo", + os: $system_to_gh_platform[.system] + }) + | sort_by(.package | ascii_downcase) + ' +} + +save_gh_ci_matrix() { + packages_to_build=$(echo "$packages" | jq -c '. | map(select((.isCached | not) and (.allowedToFail | not)))') + matrix='{"include":'"$packages_to_build"'}' + res_path='' + if [ "${IS_INITIAL:-true}" = "true" ]; then + res_path='matrix-pre.json' + else + res_path='matrix-post.json' + fi + echo "$matrix" > "$res_path" + echo "matrix=$matrix" >> "${GITHUB_OUTPUT:-${result_dir}/gh-output.env}" +} + +convert_nix_eval_to_table_summary_json() { + is_initial="${IS_INITIAL:-true}" + echo "$packages" \ + | jq ' + def getStatus(pkg; key): + if (pkg | has(key)) + then if pkg[key].isCached + then "[✅ cached](\(pkg[key].cache_url))" + else if "'$is_initial'" == "true" + then "⏳ building..." + else "❌ build failed" end + end else "🚫 not supported" end; + + group_by(.package) + | map( + . | INDEX(.system) as $pkg + | .[0].package as $name + | { + package: $name, + "x86_64-linux": getStatus($pkg; "x86_64-linux"), + "x86_64-darwin": getStatus($pkg; "x86_64-darwin"), + "aarch64-darwin": getStatus($pkg; "aarch64-darwin"), + } + ) + | sort_by(.package)' +} + +printTableForCacheStatus() { + create_result_dirs + packages="$(eval_packages_to_json "$@")" + + save_gh_ci_matrix + + { + echo "Thanks for your Pull Request!" + echo + echo "Below you will find a summary of the cachix status of each package, for each supported platform." + echo + # shellcheck disable=SC2016 + echo '| package | `x86_64-linux` | `x86_64-darwin` | `aarch64-darwin` |' + echo '| ------- | -------------- | --------------- | ---------------- |' + convert_nix_eval_to_table_summary_json | jq -r ' + .[] | "| `\(.package)` | \(.["x86_64-linux"]) | \(.["x86_64-darwin"]) | \(.["aarch64-darwin"]) |" + ' + echo + } > comment.md +} + +printTableForCacheStatus "$@" + diff --git a/scripts/nix-eval-jobs.sh b/scripts/nix-eval-jobs.sh new file mode 100755 index 00000000..913f91e4 --- /dev/null +++ b/scripts/nix-eval-jobs.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root_dir="$(git rev-parse --show-toplevel)" +result_dir="$root_dir/.result" +gc_roots_dir="$result_dir/gc-roots" + +# shellcheck source=./system-info.sh +source "$root_dir/scripts/system-info.sh" + +create_result_dirs() { + mkdir -p "$result_dir" "$gc_roots_dir" +} + +nix_eval_jobs() { + flake_attr="$1" + + get_platform + get_available_memory_mb + get_nix_eval_worker_count + + { + ( + set -x + nix-eval-jobs \ + --quiet \ + --option warn-dirty false \ + --check-cache-status \ + --gc-roots-dir "$gc_roots_dir" \ + --workers "$max_workers" \ + --max-memory-size "$max_memory_mb" \ + --flake "$root_dir#$flake_attr" + ) \ + | tee /dev/fd/3 \ + | stdbuf -i0 -o0 -e0 jq --color-output -c '{ attr, isCached, out: .outputs.out }' > /dev/stderr + } 3>&1 2> >( + grep -vP "(SQLite database|warning: unknown setting 'allowed-users'|warning: unknown setting 'trusted-users')" \ + >&2 + ) +} + +nix_eval_for_all_systems() { + flake_pre="$1" + flake_post="${2:+.$2}" + + systems=( {x86_64-linux,{x86_64,aarch64}-darwin} ) + + for system in "${systems[@]}"; do + nix_eval_jobs "${flake_pre}.${system}${flake_post}" + done +} + diff --git a/scripts/system-info.sh b/scripts/system-info.sh new file mode 100644 index 00000000..6f841fbc --- /dev/null +++ b/scripts/system-info.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +get_platform() { + case "$(uname -s).$(uname -m)" in + Linux.x86_64) + export system=x86_64-linux + export is_linux=true + export is_darwin=false + ;; + Linux.i?86) + export system=i686-linux + export is_linux=true + export is_darwin=false + ;; + Linux.aarch64) + export system=aarch64-linux + export is_linux=true + export is_darwin=false + ;; + Linux.armv6l_linux) + export system=armv6l-linux + export is_linux=true + export is_darwin=false + ;; + Linux.armv7l_linux) + export system=armv7l-linux + export is_linux=true + export is_darwin=false + ;; + Darwin.x86_64) + export system=x86_64-darwin + export is_linux=false + export is_darwin=true + ;; + Darwin.arm64|Darwin.aarch64) + system=aarch64-darwin + export is_linux=false + export is_darwin=true + ;; + *) error "sorry, there is no binary distribution of Nix for your platform";; + esac +} + +get_nix_eval_worker_count() { + if [[ -z "${MAX_WORKERS:-}" ]]; then + available_parallelism="$(nproc)" + export max_workers="$((available_parallelism < 8 ? available_parallelism : 8))" + else + export max_workers="$MAX_WORKERS" + fi +} + +get_available_memory_mb() { + if [ "$is_darwin" = 'true' ]; then + free_pages="$(vm_stat | grep 'Pages free:' | tr -s ' ' | cut -d ' ' -f 3 | tr -d '.')" + inactive_pages="$(vm_stat | grep 'Pages inactive:' | tr -s ' ' | cut -d ' ' -f 3 | tr -d '.')" + pages="$((free_pages + inactive_pages))" + page_size="$(pagesize)" + export max_memory_mb="${MAX_MEMORY:-$(((pages * page_size) / 1024 / 1024 ))}" + else + free="$(< /proc/meminfo grep MemFree | tr -s ' ' | cut -d ' ' -f 2)" + cached="$(< /proc/meminfo grep Cached | grep -v SwapCached | tr -s ' ' | cut -d ' ' -f 2)" + buffers="$(< /proc/meminfo grep Buffers | tr -s ' ' | cut -d ' ' -f 2)" + shmem="$(< /proc/meminfo grep Shmem: | tr -s ' ' | cut -d ' ' -f 2)" + export max_memory_mb="${MAX_MEMORY:-$(((free + cached + buffers + shmem) / 1024 ))}" + fi +}