name: Check changed Nix files with nixf-tidy (experimental) on: pull_request_target: types: [opened, synchronize, reopened, edited] permissions: contents: read jobs: nixos: name: exp-nixf-tidy-check runs-on: ubuntu-latest if: "!contains(github.event.pull_request.title, '[skip treewide]')" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # pull_request_target checks out the base branch by default ref: refs/pull/${{ github.event.pull_request.number }}/merge # Fetches the merge commit and its parents fetch-depth: 2 - name: Checking out base branch run: | base=$(mktemp -d) baseRev=$(git rev-parse HEAD^1) git worktree add "$base" "$baseRev" echo "baseRev=$baseRev" >> "$GITHUB_ENV" echo "base=$base" >> "$GITHUB_ENV" - name: Get Nixpkgs revision for nixf run: | # pin to a commit from nixpkgs-unstable to avoid e.g. building nixf # from staging # This should not be a URL, because it would allow PRs to run arbitrary code in CI! rev=$(jq -r .rev ci/pinned-nixpkgs.json) echo "url=https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz" >> "$GITHUB_ENV" - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 with: # explicitly enable sandbox extra_nix_config: sandbox = true nix_path: nixpkgs=${{ env.url }} - name: Install nixf and jq # provided jq is incompatible with our expression run: "nix-env -f '' -iAP nixf jq" - name: Check that Nix files pass nixf-tidy run: | # Filtering error messages we don't like nixf_wrapper(){ nixf-tidy --variable-lookup < "$1" | jq -r ' [ "sema-escaping-with" ] as $ignored_errors|[.[]|select(.sname as $s|$ignored_errors|index($s)|not)] ' } failedFiles=() # Don't report errors to file overview # to avoid duplicates when editing title and description if [[ "${{ github.event.action }}" == 'edited' ]] && [[ -z "${{ github.event.edited.changes.base }}" ]]; then DONT_REPORT_ERROR=1 else DONT_REPORT_ERROR= fi # TODO: Make this more parallel # Loop through all Nix files touched by the PR while readarray -d '' -n 2 entry && (( ${#entry[@]} != 0 )); do type=${entry[0]} file=${entry[1]} case $type in A*) source="" dest=$file ;; M*) source=$file dest=$file ;; C*|R*) source=$file read -r -d '' dest ;; *) echo "Ignoring file $file with type $type" continue esac if [[ -n "$source" ]] && [[ "$(nixf_wrapper ${{ env.base }}/"$source")" != '[]' ]] 2>/dev/null; then echo "Ignoring file $file because it doesn't pass nixf-tidy in the base commit" echo # insert blank line else nixf_report="$(nixf_wrapper "$dest")" if [[ "$nixf_report" != '[]' ]]; then echo "$dest doesn't pass nixf-tidy. Reported by nixf-tidy:" errors=$(echo "$nixf_report" | jq -r --arg dest "$dest" ' def getLCur: "line=" + (.line+1|tostring) + ",col=" + (.column|tostring); def getRCur: "endLine=" + (.line+1|tostring) + ",endColumn=" + (.column|tostring); def getRange: "file=\($dest)," + (.lCur|getLCur) + "," + (.rCur|getRCur); def getBody: . as $top|(.range|getRange) + ",title="+ .sname + "::" + (.message|sub("{}" ; ($top.args.[]|tostring))); def getNote: "\n::notice " + (.|getBody); def getMessage: "::error " + (.|getBody) + (if (.notes|length)>0 then ([.notes.[]|getNote]|add) else "" end); .[]|getMessage ') if [[ -z "$DONT_REPORT_ERROR" ]]; then echo "$errors" else # just print in plain text echo "${errors/::/}" echo # add one empty line fi failedFiles+=("$dest") fi fi done < <(git diff -z --name-status ${{ env.baseRev }} -- '*.nix') if [[ -n "$DONT_REPORT_ERROR" ]]; then echo "Edited the PR but didn't change the base branch, only the description/title." echo "Not reporting errors again to avoid duplication." echo # add one empty line fi if (( "${#failedFiles[@]}" > 0 )); then echo "Some new/changed Nix files don't pass nixf-tidy." echo "See ${{ github.event.pull_request.html_url }}/files for reported errors." echo "If you believe this is a false positive, ping @Aleksanaa and @inclyc in this PR." exit 1 fi