name: Lockfile diff

# Advisory PR comment showing the *semantic* diff of package-lock.json
# changes — which packages were added/removed/updated and their versions.
# The raw textual diff of a lockfile is unreadable (npm reorders entries
# and rewrites integrity hashes), so scripts/ci/lockfile_diff.py parses
# the ``packages`` map at the merge base and at HEAD and set-diffs the
# {install path: version} maps instead.
#
# The semantic diff is exposed as a workflow_call output ``review_status``
# (a JSON array in the unified status format) and an artifact
# (``lockfile-diff`` containing the markdown fragment) for the step
# summary.
#
# Never blocking — this is review signal, not enforcement.

on:
  workflow_call:
    outputs:
      changed:
        description: Whether package-lock.json changed relative to the target branch.
        value: ${{ jobs.diff.outputs.changed }}
      review_status:
        description: JSON array of review status objects for the unified PR comment.
        value: ${{ jobs.diff.outputs.review_status }}

permissions:
  contents: read

concurrency:
  group: lockfile-diff-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  diff:
    name: package-lock.json semantic diff
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      changed: ${{ steps.diff.outputs.changed }}
      review_status: ${{ steps.emit-status.outputs.review_status }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0 # need history for the merge base

      - name: Generate semantic lockfile diff
        id: diff
        run: |
          set -euo pipefail
          # Three-dot semantics by hand: diff from the merge base with the
          # target branch to the PR head, so changes that landed on main
          # after the branch point don't show up as this PR's doing.
          BASE_SHA=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
          echo "Merge base: ${BASE_SHA}"
          python3 scripts/ci/lockfile_diff.py \
            --base "$BASE_SHA" \
            --head HEAD \
            --output /tmp/lockfile-diff.md
          if [ -s /tmp/lockfile-diff.md ]; then
            echo "changed=true" >> "$GITHUB_OUTPUT"
            {
              echo "## package-lock.json semantic diff"
              echo ""
              cat /tmp/lockfile-diff.md
            } >> "$GITHUB_STEP_SUMMARY"
          else
            echo "changed=false" >> "$GITHUB_OUTPUT"
            : > /tmp/lockfile-diff.md
          fi

      - name: Emit review_status
        id: emit-status
        run: |
          set -euo pipefail
          CHANGED="${{ steps.diff.outputs.changed }}"
          STATUS="[]"

          if [ "$CHANGED" = "true" ]; then
            CONTENT=$(cat /tmp/lockfile-diff.md | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
            STATUS="[{\"source\":\"lockfile-diff\",\"results\":[{\"kind\":\"action_required\",\"title\":\"package-lock.json\",\"summary\":\"Locked npm dependency versions changed.\",\"detail\":${CONTENT},\"how_to_fix\":\"Add the \`ci-reviewed\` label after verifying the version changes are expected.\"}]}"
          else
            STATUS="[]"
          fi

          echo "review_status=${STATUS}" >> "$GITHUB_OUTPUT"

      - name: Upload diff artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: lockfile-diff
          path: /tmp/lockfile-diff.md
          retention-days: 1
          overwrite: true
