# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: dependabot/refresh-cargo-lockfiles

on:
  pull_request_target:
    types: [opened, synchronize, reopened]
    branches: ["main"]

concurrency:
  group: dependabot-refresh-cargo-lockfiles-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  refresh-cargo-lockfiles:
    permissions:
      contents: write
    if: >-
      github.event.pull_request.user.login == 'dependabot[bot]' &&
      github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest

    steps:
      # SECURITY: This checks out untrusted PR code at the EXACT commit that
      # triggered the event (immutable SHA, not mutable branch ref) to avoid
      # TOCTOU if the branch moves between event dispatch and checkout.
      # ONLY cargo update and cargo metadata (which do NOT execute build
      # scripts) may run against this checkout. Do NOT add cargo build/check/
      # test/run steps.
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v4.2.2
        with:
          repository: ${{ github.event.pull_request.head.repo.full_name }}
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 1
          persist-credentials: false

      - name: Setup Rust toolchain
        run: |
          rustup toolchain install 1.92.0 --profile minimal
          rustup override set 1.92.0
          cargo --version
          rustc --version

      - name: Refresh all Cargo lockfiles
        shell: bash
        env:
          BASE_REF: ${{ github.base_ref }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          set -euo pipefail

          # Validate inputs (defense-in-depth against expression injection).
          if ! git check-ref-format "refs/heads/$BASE_REF" > /dev/null 2>&1; then
            echo "::error::Invalid base ref format: '$BASE_REF'"
            exit 1
          fi
          if [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
            echo "::error::Invalid head SHA format: '$HEAD_SHA'"
            exit 1
          fi

          # Fetch the base branch into its remote-tracking ref so we can diff.
          # fetch-depth: 0 on the head ref doesn't guarantee the base branch
          # tip is reachable if it has diverged.
          git fetch --no-tags --depth=1 origin "refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"

          # Diff against the base branch tip to detect Cargo changes.
          # False positives (base advanced) are harmless — they just trigger
          # a no-op refresh since we update ALL lockfiles unconditionally.
          mapfile -t changed_files < <(git diff --name-only "origin/${BASE_REF}" "$HEAD_SHA" -- ':(glob)**/Cargo.toml' ':(glob)**/Cargo.lock')

          if [ "${#changed_files[@]}" -eq 0 ]; then
            echo "No Cargo manifest or lockfile changes detected."
            exit 0
          fi

          # Always refresh ALL lockfiles when any Cargo change is detected.
          # Dependabot security updates bypass grouping and create per-directory
          # PRs, causing version skew if we only refresh the affected directory.
          # See: https://github.com/dependabot/dependabot-core/issues/7547
          #
          # We use `cargo update` (not `cargo metadata`) to actually propagate
          # version bumps across lockfiles. `cargo update` only resolves
          # dependencies and rewrites Cargo.lock — it does NOT execute build
          # scripts, so it is safe to run on untrusted PR code.
          all_manifests=(
            "Cargo.toml"
            "bindings/ffi/Cargo.toml"
            "bindings/java/Cargo.toml"
            "bindings/python/Cargo.toml"
            "bindings/ruby/Cargo.toml"
            "bindings/wasm/Cargo.toml"
          )

          for manifest in "${all_manifests[@]}"; do
            echo "Refreshing lockfile for $manifest"
            cargo update --manifest-path "$manifest"
          done

      - name: Commit lockfile refresh
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
          HEAD_REF: ${{ github.event.pull_request.head.ref }}
        run: |
          set -euo pipefail

          # Validate ref format (defense-in-depth against expression injection).
          if ! git check-ref-format "refs/heads/$HEAD_REF" > /dev/null 2>&1; then
            echo "::error::Invalid head ref format: '$HEAD_REF'"
            exit 1
          fi

          mapfile -t lockfiles < <(git ls-files -m -o --exclude-standard -- ':(glob)**/Cargo.lock')

          for lockfile in "${lockfiles[@]}"; do
            git add "$lockfile"
          done

          if git diff --cached --quiet; then
            echo "No Cargo lockfile changes required."
            exit 0
          fi

          auth_header=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
          trap 'git config --unset-all http.https://github.com/.extraheader' EXIT
          git config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git commit -m "build(deps): refresh Cargo lockfiles"
          git push origin "HEAD:refs/heads/${HEAD_REF}"
