current commit
261110.12 · tools: aggregate Nix changes in a table
# This action requires the following secrets to be set on the repository:
#   GH_USER_TOKEN: GitHub user token, to be used by ncu and to push changes
name: Comment on PR on Nix changes

on:
  workflow_run:
    workflows: [Nix files edited]
    types: [completed]

permissions: {}

jobs:
  aggregate-results:
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
    name: Aggregate results
    runs-on: ubuntu-slim
    permissions:
      pull-requests: write
      statuses: write
    steps:
      - name: Download artefacts
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333  # v8.0.2
        with:
          run-id: ${{ github.event.workflow_run.id }}
          # Using privileged token to be able to download file from a different workflow
          github-token: ${{ secrets.GH_USER_TOKEN }}

      - name: Fetch PR data
        id: pr-data
        run: |
          mv requisites-*/* .
          ls
          if grep -nvHE '^/nix/store/[0-9a-df-np-sv-z]{32}-[A-Za-z0-9?=._+-]+$' requisites-*.list; then
            echo 'Unexpected line format in requisites list; refusing to process.' >&2
            exit 1
          fi
          echo "NUMBER=$(grep -E '^[0-9]+$' pr_number.txt || true)" >> "$GITHUB_OUTPUT"

      - name: Aggregate results
        if: ${{ steps.pr-data.outputs.NUMBER }}
        run: |
          set -x
          > requisites-before.list
          > requisites-after.list
          {
            echo '| Platform | Number of requisites | Proportion of new derivations |'
            echo '| :-: | -: | -: |'
            TOTAL_BEFORE=0
            TOTAL_AFTER=0
            TOTAL_CHANGED=0
            QUOTE='`'
            for PLATFORM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
              if [ ! -f "requisites-${PLATFORM}-before.list" ] || [ ! -f "requisites-${PLATFORM}-after.list" ]; then
                echo "| ${QUOTE}${PLATFORM}${QUOTE} | _missing data_ | _missing data_ |"
                continue
              fi
              BEFORE=$(wc -l < "requisites-${PLATFORM}-before.list")
              TOTAL_BEFORE=$(($TOTAL_BEFORE + $BEFORE))
              AFTER=$(wc -l < "requisites-${PLATFORM}-after.list")
              TOTAL_AFTER=$(($TOTAL_AFTER + $AFTER))
              DIFF="_no changes_"
              [ "$BEFORE" = "$AFTER" ] || DIFF="**$([ "$AFTER" -lt "$BEFORE" ] || echo '+')$(($AFTER - $BEFORE))**"
              CHANGED=$(git diff --no-index "requisites-${PLATFORM}-before.list" "requisites-${PLATFORM}-after.list" | grep -c '^+[^+]' || true)
              TOTAL_CHANGED=$(($TOTAL_CHANGED + $CHANGED))
              PERCENT=100
              [ "$CHANGED" = "$AFTER" ] || {
                PERMILLE=$((1000 * $CHANGED / $AFTER))
                PERCENT="$((PERMILLE / 10)).$((PERMILLE % 10))"
              }
              echo "| ${QUOTE}${PLATFORM}${QUOTE} | $BEFORE -> $AFTER => $DIFF | $CHANGED / $AFTER = $PERCENT% |"
              for STATE in before after; do
                # Split store paths into package name, output (or source archive extension), and version.
                awk -v OFS='\t' -v PLATFORM="$PLATFORM" '{
                  name = substr($0, 45)
                  pname = name
                  output = "out"
                  version = "_unversioned_"
                  if (match(name, /-[^A-Za-z]/)) {
                    pname = substr(name, 1, RSTART - 1)
                    version = substr(name, RSTART + 1)
                    if (match(version, /-[a-z]+$/) || match(version, /\.(tar(\.[a-z0-9]+)?|tgz|tbz2?|txz|zip|patch|diff)$/)) {
                      output = substr(version, RSTART + 1)
                      version = substr(version, 1, RSTART - 1)
                    }
                  }
                  print pname, output, version, PLATFORM, $0
                }' "requisites-${PLATFORM}-${STATE}.list" >> "requisites-${STATE}.list"
              done
            done

            DIFF="_no changes_"
            [ "$TOTAL_BEFORE" = "$TOTAL_AFTER" ] || DIFF="**$([ "$TOTAL_AFTER" -lt "$TOTAL_BEFORE" ] || echo '+')$(($TOTAL_AFTER - $TOTAL_BEFORE))**"
            PERCENT=100
            [ "$TOTAL_CHANGED" = "$TOTAL_AFTER" ] || {
              PERMILLE=$((1000 * $TOTAL_CHANGED / $TOTAL_AFTER))
              PERCENT="$((PERMILLE / 10)).$((PERMILLE % 10))"
            }
            echo "| **Total** | $TOTAL_BEFORE -> $TOTAL_AFTER => $DIFF | $TOTAL_CHANGED / $TOTAL_AFTER = $PERCENT% |"
          } > comment.md
          sort requisites-before.list > requisites-before.sort
          sort requisites-after.list > requisites-after.sort
          if git diff -U0 --no-color --no-index requisites-before.sort requisites-after.sort > requisites.diff; then
            echo 'No changes detected.'
          else
            awk -F '\t' '
              function insert(list, v,    n, a, i, out, done) {
                n = split(list, a, " ")
                for (i = 1; i <= n; i++) if (a[i] == v) return list
                out = ""; done = 0
                for (i = 1; i <= n; i++) {
                  if (!done && v < a[i]) { out = out " " v; done = 1 }
                  out = out " " a[i]
                }
                if (!done) out = out " " v
                return substr(out, 2)
              }
              function join(list, pre, post,    s) {
                s = pre list post
                gsub(/ /, post ", " pre, s)
                return s
              }
              /^[-+]/ && NF == 5 {
                key = substr($1, 2) SUBSEP $2 SUBSEP $4
                if (substr($1, 1, 1) == "-") removed[key] = insert(removed[key], $3)
                else added[key] = insert(added[key], $3)
              }
              END {
                for (key in removed) if (!(key in added)) added[key] = ""
                for (key in added) {
                  split(key, k, SUBSEP)
                  r = (key in removed) ? removed[key] : ""
                  a = added[key]
                  if (r == a) change = "_no version change_: " join(a)
                  else if (r == "") change = "_added_: " join(a, "**", "**")
                  else if (a == "") change = "_removed_: " join(r)
                  else change = join(r) " -> " join(a, "**", "**")
                  platforms[k[1] SUBSEP k[2] SUBSEP change] = insert(platforms[k[1] SUBSEP k[2] SUBSEP change], k[3])
                }
                for (key in platforms) {
                  split(key, k, SUBSEP)
                  row = k[1] SUBSEP k[3] SUBSEP join(platforms[key])
                  outputs[row] = insert(outputs[row], k[2])
                }
                for (row in outputs) {
                  split(row, k, SUBSEP)
                  n = split(outputs[row], o, " ")
                  name = ""
                  for (i = 1; i <= n; i++) name = name ", `" k[1] (o[i] == "out" ? "" : "." o[i]) "`"
                  rank = k[2] ~ /^_added_/ ? 0 : k[2] ~ /^_removed_/ ? 2 : k[2] ~ /^_no version change_/ ? 3 : 1
                  print rank "\t| " substr(name, 3) " | " k[2] " | " k[3] " |"
                }
              }
            ' requisites.diff > versions.tsv
            {
              echo
              echo '<details><summary>Version changes</summary>'
              echo
              echo '| Package | Version | Platforms |'
              echo '| :- | :- | :- |'
              LC_ALL=C sort -t '`' -k1,1 -k2,2 -k3 versions.tsv | cut -f2-
              echo
              echo '</details>'
            } >> comment.md
            gh pr comment -R "$GITHUB_REPOSITORY" "$PR_NUMBER" -F comment.md
          fi
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ steps.pr-data.outputs.NUMBER }}

      - name: Report status on the PR
        if: ${{ always() }}
        run: |
          gh api --method POST "repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \
            -f state="$STATE" \
            -f context="$GITHUB_WORKFLOW" \
            -f description="$DESCRIPTION" \
            -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
        env:
          GH_TOKEN: ${{ github.token }}
          HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
          STATE: ${{ job.status == 'success' && 'success' || 'failure' }}
          DESCRIPTION: ${{ job.status == 'success' && 'Nix requisites changes aggregated' || 'Failed to aggregate Nix requisites changes' }}