diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 280ed141576cd..90be4670e414b 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -1,6 +1,16 @@ name: Eval -on: pull_request_target +on: + pull_request_target: + push: + # Keep this synced with ci/request-reviews/dev-branches.txt + branches: + - master + - staging + - release-* + - staging-* + - haskell-updates + - python-updates permissions: contents: read @@ -11,6 +21,7 @@ jobs: runs-on: ubuntu-latest outputs: mergedSha: ${{ steps.merged.outputs.mergedSha }} + baseSha: ${{ steps.baseSha.outputs.baseSha }} systems: ${{ steps.systems.outputs.systems }} steps: # Important: Because of `pull_request_target`, this doesn't check out the PR, @@ -24,14 +35,22 @@ jobs: id: merged env: GH_TOKEN: ${{ github.token }} + GH_EVENT: ${{ github.event_name }} run: | - if mergedSha=$(base/ci/get-merge-commit.sh ${{ github.repository }} ${{ github.event.number }}); then - echo "Checking the merge commit $mergedSha" - echo "mergedSha=$mergedSha" >> "$GITHUB_OUTPUT" - else - # Skipping so that no notifications are sent - echo "Skipping the rest..." - fi + case "$GH_EVENT" in + push) + echo "mergedSha=${{ github.sha }}" >> "$GITHUB_OUTPUT" + ;; + pull_request_target) + if mergedSha=$(base/ci/get-merge-commit.sh ${{ github.repository }} ${{ github.event.number }}); then + echo "Checking the merge commit $mergedSha" + echo "mergedSha=$mergedSha" >> "$GITHUB_OUTPUT" + else + # Skipping so that no notifications are sent + echo "Skipping the rest..." + fi + ;; + esac rm -rf base - name: Check out the PR at the test merge commit uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -39,8 +58,16 @@ jobs: if: steps.merged.outputs.mergedSha with: ref: ${{ steps.merged.outputs.mergedSha }} + fetch-depth: 2 path: nixpkgs + - name: Determine base commit + if: github.event_name == 'pull_request_target' && steps.merged.outputs.mergedSha + id: baseSha + run: | + baseSha=$(git -C nixpkgs rev-parse HEAD^1) + echo "baseSha=$baseSha" >> "$GITHUB_OUTPUT" + - name: Install Nix uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 if: steps.merged.outputs.mergedSha @@ -105,6 +132,8 @@ jobs: name: Process runs-on: ubuntu-latest needs: [ outpaths, attrs ] + outputs: + baseRunId: ${{ steps.baseRunId.outputs.baseRunId }} steps: - name: Download output paths and eval stats for all systems uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 @@ -124,18 +153,93 @@ jobs: - name: Combine all output paths and eval stats run: | nix-build nixpkgs/ci -A eval.combine \ - --arg resultsDir ./intermediate + --arg resultsDir ./intermediate \ + -o prResult - name: Upload the combined results uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: result - path: result/* + path: prResult/* + - name: Get base run id + if: needs.attrs.outputs.baseSha + id: baseRunId + run: | + # Get the latest eval.yml workflow run for the PR's base commit + if ! run=$(gh api --method GET /repos/"$REPOSITORY"/actions/workflows/eval.yml/runs \ + -f head_sha="$BASE_SHA" \ + --jq '.workflow_runs | sort_by(.run_started_at) | .[-1]') \ + || [[ -z "$run" ]]; then + echo "Could not find an eval.yml workflow run for $BASE_SHA, cannot make comparison" + exit 0 + fi + echo "Comparing against $(jq .html_url <<< "$run")" + runId=$(jq .id <<< "$run") + conclusion=$(jq -r .conclusion <<< "$run") + + while [[ "$conclusion" == null ]]; do + echo "Workflow not done, waiting 10 seconds before checking again" + sleep 10 + conclusion=$(gh api /repos/"$REPOSITORY"/actions/runs/"$runId" --jq '.conclusion') + done + + if [[ "$conclusion" != "success" ]]; then + echo "Workflow was not successful, cannot make comparison" + exit 0 + fi - # TODO: Run this workflow also on `push` (on at least the main development branches) - # Then add an extra step here that waits for the base branch (not the merge base, because that could be very different) - # to have completed the eval, then use - # gh api --method GET /repos/NixOS/nixpkgs/actions/workflows/eval.yml/runs -f head_sha= - # and follow it to the artifact results, where you can then download the outpaths.json from the base branch - # That can then be used to compare the number of changed paths, get evaluation stats and ping appropriate reviewers + echo "baseRunId=$runId" >> "$GITHUB_OUTPUT" + env: + REPOSITORY: ${{ github.repository }} + BASE_SHA: ${{ needs.attrs.outputs.baseSha }} + GH_TOKEN: ${{ github.token }} + + - uses: actions/download-artifact@v4 + if: steps.baseRunId.outputs.baseRunId + with: + name: result + path: baseResult + github-token: ${{ github.token }} + run-id: ${{ steps.baseRunId.outputs.baseRunId }} + + - name: Compare against the base branch + if: steps.baseRunId.outputs.baseRunId + run: | + nix-build nixpkgs/ci -A eval.compare \ + --arg beforeResultDir ./baseResult \ + --arg afterResultDir ./prResult \ + -o comparison + + # TODO: Request reviews from maintainers for packages whose files are modified in the PR + + - name: Upload the combined results + if: steps.baseRunId.outputs.baseRunId + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: comparison + path: comparison/* + + # Separate job to have a very tightly scoped PR write token + tag: + name: Tag + runs-on: ubuntu-latest + needs: process + if: needs.process.outputs.baseRunId + permissions: + pull-requests: write + steps: + - name: Download process result + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: comparison + path: comparison + + - name: Tagging pull request + run: | + gh api \ + --method POST \ + /repos/${{ github.repository }}/issues/${{ github.event.number }}/labels \ + --input <(jq -c '{ labels: .labels }' comparison/changed-paths.json) + env: + GH_TOKEN: ${{ github.token }} diff --git a/ci/eval/default.nix b/ci/eval/default.nix index 3f31faef73ae3..12be003cd0791 100644 --- a/ci/eval/default.nix +++ b/ci/eval/default.nix @@ -238,6 +238,22 @@ let jq -s from_entries > $out/stats.json ''; + compare = + { beforeResultDir, afterResultDir }: + runCommand "compare" { + nativeBuildInputs = [ + jq + ]; + } '' + mkdir $out + jq -n -f ${./filter.jq} \ + --slurpfile before ${beforeResultDir}/outpaths.json \ + --slurpfile after ${afterResultDir}/outpaths.json \ + > $out/changed-paths.json + + # TODO: Compare eval stats + ''; + full = { # Whether to evaluate just a single system, by default all are evaluated @@ -268,6 +284,7 @@ in attrpathsSuperset singleSystem combine + compare # The above three are used by separate VMs in a GitHub workflow, # while the below is intended for testing on a single local machine full diff --git a/ci/eval/filter.jq b/ci/eval/filter.jq new file mode 100644 index 0000000000000..fb94d525bc039 --- /dev/null +++ b/ci/eval/filter.jq @@ -0,0 +1,152 @@ +# Turns +# +# { +# "hello.aarch64-linux": "a", +# "hello.x86_64-linux": "b", +# "hello.aarch64-darwin": "c", +# "hello.x86_64-darwin": "d" +# } +# +# into +# +# { +# "hello": { +# "linux": { +# "aarch64": "a", +# "x86_64": "b" +# }, +# "darwin": { +# "aarch64": "c", +# "x86_64": "d" +# } +# } +# } +# +# while filtering out any attribute paths that don't match this pattern +def expand_system: + to_entries + | map( + .key |= split(".") + | select(.key | length > 1) + | .double = (.key[-1] | split("-")) + | select(.double | length == 2) + ) + | group_by(.key[0:-1]) + | map( + { + key: .[0].key[0:-1] | join("."), + value: + group_by(.double[1]) + | map( + { + key: .[0].double[1], + value: map(.key = .double[0]) | from_entries + } + ) + | from_entries + }) + | from_entries + ; + +# Transposes +# +# { +# "a": [ "x", "y" ], +# "b": [ "x" ], +# } +# +# into +# +# { +# "x": [ "a", "b" ], +# "y": [ "a" ] +# } +def transpose: + [ + to_entries[] + | { + key: .key, + value: .value[] + } + ] + | group_by(.value) + | map({ + key: .[0].value, + value: map(.key) + }) + | from_entries + ; + +# Computes the key difference for two objects: +# { +# added: [ ], +# removed: [ ], +# changed: [ ], +# } +# +def diff($before; $after): + { + added: $after | delpaths($before | keys | map([.])) | keys, + removed: $before | delpaths($after | keys | map([.])) | keys, + changed: + $before + | to_entries + | map( + $after."\(.key)" as $after2 + | select( + # Filter out attributes that don't exist anymore + ($after2 != null) + and + # Filter out attributes that are the same as the new value + (.value != $after2) + ) + | .key + ) + } + ; + +($before[0] | expand_system) as $before +| ($after[0] | expand_system) as $after +| .attrdiff = diff($before; $after) +| .rebuildsByKernel = ( + .attrdiff.changed + | map({ + key: ., + value: diff($before."\(.)"; $after."\(.)").changed + }) + | from_entries + | transpose +) +| .rebuildCountByKernel = ( + .rebuildsByKernel + | with_entries(.value |= length) + | pick(.linux, .darwin) + | { + linux: (.linux // 0), + darwin: (.darwin // 0), + } +) +| .labels = ( + .rebuildCountByKernel + | to_entries + | map( + "10.rebuild-\(.key): " + + if .value == 0 then + "0" + elif .value <= 10 then + "1-10" + elif .value <= 100 then + "11-100" + elif .value <= 500 then + "101-500" + elif .value <= 1000 then + "501-1000" + elif .value <= 2500 then + "1001-2500" + elif .value <= 5000 then + "2501-5000" + else + "5000+" + end + ) +) diff --git a/ci/request-reviews/dev-branches.txt b/ci/request-reviews/dev-branches.txt index 2282529881bab..b34092546f184 100644 --- a/ci/request-reviews/dev-branches.txt +++ b/ci/request-reviews/dev-branches.txt @@ -1,5 +1,6 @@ # Trusted development branches: # These generally require PRs to update and are built by Hydra. +# Keep this synced with the branches in .github/workflows/eval.yml master staging release-*