echoes 0.10.2

Find duplicated Rust doctests, functions and contexts
Documentation
name: cargo-echoes
description: Find duplicated Rust doctests, functions and code blocks, and review the copies a pull request adds

branding:
  icon: 'copy'
  color: 'orange'
inputs:
  threshold:
    description: Report duplicate groups with at least this many sites
    required: false
    default: '2'
  min-tokens:
    description: Skip doctest bodies with fewer tokens than this
    required: false
    default: '0'
  functions:
    description: Also check for duplicated functions
    required: false
    default: 'false'
  fn-min-tokens:
    description: Skip functions whose body counts fewer tokens than this, a path or an operator counting as one
    required: false
    default: ''
  context-blocks:
    description: Also check for duplicated context blocks
    required: false
    default: 'false'
  context-min-tokens:
    description: Skip context blocks with fewer tokens than this
    required: false
    default: ''
  context-select:
    description: Select the local contexts to check, empty keeps the whole workspace
    required: false
    default: ''
  context-search:
    description: Restrict the comparison corpus to one context scope
    required: false
    default: ''
  context-dependency-depth:
    description: Search dependency sources to this shortest-path depth, zero stays local
    required: false
    default: '0'
  context-dev-dependencies:
    description: Include development dependency edges
    required: false
    default: 'false'
  context-build-dependencies:
    description: Include build dependency edges
    required: false
    default: 'false'
  context-all-optional-dependencies:
    description: Explore optional dependency paths outside the selected feature configuration
    required: false
    default: 'false'
  context-features:
    description: Cargo features enabled for dependency discovery
    required: false
    default: ''
  context-all-features:
    description: Enable all features for dependency discovery
    required: false
    default: 'false'
  context-no-default-features:
    description: Disable default features for dependency discovery
    required: false
    default: 'false'
  context-target:
    description: Target triple used for dependency discovery
    required: false
    default: ''
  context-cache-dir:
    description: Persistent dependency index directory, empty for a run-local directory
    required: false
    default: ''
  context-check-latest:
    description: Check the latest published release of indexed dependencies
    required: false
    default: 'true'
  context-fail-on-reuse:
    description: Make dependency reuse findings fail the duplicate gate
    required: false
    default: 'false'
  all-targets:
    description: Also scan bin and example targets
    required: false
    default: 'false'
  package:
    description: Restrict the scan to one workspace member by name
    required: false
    default: ''
  version:
    description: The echoes version to install, empty for the latest
    required: false
    default: ''
  install:
    description: Install echoes, false uses the cargo echoes already on the path
    required: false
    default: 'true'
  cwd:
    description: The workspace directory to scan
    required: false
    default: ''
  pr-number:
    description: Post the findings as a pull request review on this PR instead of failing the job, in post mode outside workflow_run the PR the payload must name
    required: false
    default: ''
  dry-run:
    description: Render the review instead of posting it
    required: false
    default: 'false'
  only-new:
    description: In pull request mode report only duplicates this pull request introduces
    required: false
    default: 'true'
  annotations:
    description: Annotate each copy to remove, which needs no token
    required: false
    default: 'true'
  mode:
    description: "review posts the findings in one job, while prepare and post split it so a workflow_run job can review a fork's pull request"
    required: false
    default: 'review'
  payload:
    description: Directory for prepare output or post input, holding one prepared scan or up to 64 immediate payload subdirectories, ignoring linked subdirectories. Each payload is checked and posted independently, and any invalid payload or empty collection fails the step.
    required: false
    default: ''
  max-comments:
    description: Inline comments posted per review, the omitted copies go to the review body and the run summary
    required: false
    default: '50'
  exclude:
    description: Leave out files matching these gitignore-style globs, one per line, relative to the workspace or every repository root, or to one repository after an owner/repo prefix with a colon
    required: false
    default: ''
  organizations:
    description: Scan every repository of these GitHub organizations or users instead of the workspace, one per line
    required: false
    default: ''
  repositories:
    description: Scan these OWNER/REPO repositories instead of the workspace, together with any organizations, one per line
    required: false
    default: ''

runs:
  using: 'composite'
  steps:
    - name: Check the repository selection
      if: ${{ inputs.organizations != '' || inputs.repositories != '' }}
      shell: bash
      env:
        PR_NUMBER: ${{ inputs.pr-number }}
        DRY_RUN: ${{ inputs.dry-run }}
        MODE: ${{ inputs.mode }}
      run: |
        if [ -n "$PR_NUMBER" ] || [ "$DRY_RUN" = "true" ] || [ "$MODE" != "review" ]; then
          echo "::error title=echoes::organizations and repositories only run as a gate, without pr-number, dry-run or mode"
          exit 2
        fi

    - name: Install echoes
      if: ${{ inputs.mode != 'post' && inputs.install == 'true' }}
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
        VERSION: ${{ inputs.version }}
      run: |
        set -euo pipefail
        repo="LucaCappelletti94/cargo-echoes"
        tag=''
        [ -n "$VERSION" ] && tag="v$VERSION"

        # Keep these targets in sync with the release matrix in .github/workflows/release.yml.
        case "$RUNNER_OS/$RUNNER_ARCH" in
          Linux/X64)   target=x86_64-unknown-linux-gnu ;;
          Linux/ARM64) target=aarch64-unknown-linux-gnu ;;
          macOS/X64)   target=x86_64-apple-darwin ;;
          macOS/ARM64) target=aarch64-apple-darwin ;;
          Windows/X64) target=x86_64-pc-windows-msvc ;;
          *)           target='' ;;
        esac

        if [ -n "$target" ]; then
          asset="cargo-echoes-$target.tar.gz"
          dest="$RUNNER_TEMP/echoes-bin"
          mkdir -p "$dest"
          # Action series releases carry no binaries and GitHub marks the newest one Latest,
          # so resolve the newest release that actually ships the asset instead of trusting Latest.
          if [ -z "$tag" ]; then
            tag="$(gh api "repos/$repo/releases?per_page=30" --paginate \
              --jq ".[] | select(.draft == false and .prerelease == false) | select([.assets[].name] | index(\"$asset\")) | .tag_name" 2>/dev/null | head -n1)" || true
          fi
          args=(--repo "$repo" --pattern "$asset" --dir "$dest" --clobber)
          [ -n "$tag" ] && args=("$tag" "${args[@]}")
          if err="$(gh release download "${args[@]}" 2>&1)"; then
            bin=cargo-echoes
            [ "$RUNNER_OS" = Windows ] && bin=cargo-echoes.exe
            tar -xzf "$dest/$asset" -C "$dest"
            # Cargo also finds subcommands in its own bin directory, so PATH stays untouched.
            bin_dir="${CARGO_HOME:-$HOME/.cargo}/bin"
            mkdir -p "$bin_dir"
            cp "$dest/$bin" "$bin_dir/$bin"
            chmod +x "$bin_dir/$bin" 2>/dev/null || true
            echo "echoes: installed the prebuilt $asset from $tag"
            exit 0
          fi
          echo "echoes: could not download $asset, falling back to source"
          printf '%s\n' "$err" | sed 's/^/echoes: /'
        else
          echo "echoes: no prebuilt binary for $RUNNER_OS/$RUNNER_ARCH, falling back to source"
        fi

        install=(echoes)
        [ -n "$tag" ] && install+=(--version "$VERSION")
        cargo install "${install[@]}"

    - name: Resolve the dependency index directory
      id: cache-dir
      if: ${{ inputs.mode != 'post' && inputs.context-dependency-depth > 0 }}
      shell: bash
      env:
        CONTEXT_CACHE_DIR: ${{ inputs.context-cache-dir }}
      run: |
        dir="${CONTEXT_CACHE_DIR:-$RUNNER_TEMP/echoes-context-index}"
        mkdir -p "$dir/public"
        echo "dir=$dir" >> "$GITHUB_OUTPUT"

    - name: Restore the dependency index
      if: ${{ inputs.mode != 'post' && inputs.context-dependency-depth > 0 }}
      uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
      with:
        path: ${{ steps.cache-dir.outputs.dir }}/public
        key: echoes-context-index-${{ runner.os }}-${{ inputs.context-target != '' && inputs.context-target || 'any' }}-v1
        restore-keys: echoes-context-index-${{ runner.os }}-

    - name: Scan for duplicates
      id: scan
      if: ${{ inputs.mode != 'post' }}
      shell: bash
      working-directory: ${{ inputs.cwd }}
      env:
        ALL_TARGETS: ${{ inputs.all-targets }}
        PACKAGE: ${{ inputs.package }}
        PR_NUMBER: ${{ inputs.pr-number }}
        DRY_RUN: ${{ inputs.dry-run }}
        THRESHOLD: ${{ inputs.threshold }}
        MIN_TOKENS: ${{ inputs.min-tokens }}
        FUNCTIONS: ${{ inputs.functions }}
        FN_MIN_TOKENS: ${{ inputs.fn-min-tokens }}
        ANNOTATIONS: ${{ inputs.annotations }}
        CONTEXT_BLOCKS: ${{ inputs.context-blocks }}
        CONTEXT_MIN_TOKENS: ${{ inputs.context-min-tokens }}
        CONTEXT_SELECT: ${{ inputs.context-select }}
        CONTEXT_SEARCH: ${{ inputs.context-search }}
        CONTEXT_DEPTH: ${{ inputs.context-dependency-depth }}
        CONTEXT_DEV: ${{ inputs.context-dev-dependencies }}
        CONTEXT_BUILD: ${{ inputs.context-build-dependencies }}
        CONTEXT_ALL_OPT: ${{ inputs.context-all-optional-dependencies }}
        CONTEXT_FEATURES: ${{ inputs.context-features }}
        CONTEXT_ALL_FEATURES: ${{ inputs.context-all-features }}
        CONTEXT_NO_DEFAULT: ${{ inputs.context-no-default-features }}
        CONTEXT_TARGET: ${{ inputs.context-target }}
        CONTEXT_CACHE_DIR: ${{ steps.cache-dir.outputs.dir }}
        CONTEXT_CHECK_LATEST: ${{ inputs.context-check-latest }}
        CONTEXT_FAIL: ${{ inputs.context-fail-on-reuse }}
        EXCLUDE: ${{ inputs.exclude }}
        ORGANIZATIONS: ${{ inputs.organizations }}
        REPOSITORIES: ${{ inputs.repositories }}
        WORKFLOW_TOKEN: ${{ github.token }}
      run: bash "$GITHUB_ACTION_PATH/scripts/scan.sh"

    - name: Scan the base commit
      if: ${{ inputs.pr-number != '' && inputs.only-new == 'true' && inputs.mode != 'post' }}
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
        PR_NUMBER: ${{ inputs.pr-number }}
        CWD: ${{ inputs.cwd }}
        MIN_TOKENS: ${{ inputs.min-tokens }}
      run: bash "$GITHUB_ACTION_PATH/scripts/base-scan.sh"

    - name: Review the findings on the pull request
      # A prepare run writes its payload even for a clean scan, the posting job downloads it, and a
      # clean review run still cleans up the threads and reviews of earlier heads.
      if: ${{ inputs.mode == 'post' || (inputs.pr-number != '' && inputs.dry-run != 'true') || (steps.scan.outputs.found == 'true' && (inputs.pr-number != '' || inputs.dry-run == 'true')) }}
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
        PR_NUMBER: ${{ inputs.pr-number }}
        DRY_RUN: ${{ inputs.dry-run }}
        ANNOTATIONS: ${{ inputs.annotations }}
        MODE: ${{ inputs.mode }}
        PAYLOAD: ${{ inputs.payload }}
        MAX_COMMENTS: ${{ inputs.max-comments }}
        EVENT_NAME: ${{ github.event_name }}
        RUN_EVENT: ${{ github.event.workflow_run.event }}
        RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
        RUN_HEAD_REPO_ID: ${{ github.event.workflow_run.head_repository.id }}
      run: |
        if [ "$MODE" = "post" ]; then
          bash "$GITHUB_ACTION_PATH/scripts/post.sh"
        else
          bash "$GITHUB_ACTION_PATH/scripts/review.sh"
        fi

    - name: Save the dependency index
      if: ${{ always() && !cancelled() && inputs.mode != 'post' && inputs.context-dependency-depth > 0 && steps.scan.outputs.index-ok == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
      uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
      with:
        path: ${{ steps.cache-dir.outputs.dir }}/public
        key: echoes-context-index-${{ runner.os }}-${{ inputs.context-target != '' && inputs.context-target || 'any' }}-v1-${{ github.run_id }}-${{ github.run_attempt }}