name: '🏷️ Gemini Automated Issue Deduplication'

on:
  issues:
    types:
      - 'opened'
      - 'reopened'
  issue_comment:
    types:
      - 'created'
  workflow_dispatch:
    inputs:
      issue_number:
        description: 'issue number to dedup'
        required: true
        type: 'number'

concurrency:
  group: '${{ github.workflow }}-${{ github.event.issue.number }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

jobs:
  find-duplicates:
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
      (github.event_name == 'issues' ||
       github.event_name == 'workflow_dispatch' ||
       (github.event_name == 'issue_comment' &&
       contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
       (github.event.comment.author_association == 'OWNER' ||
        github.event.comment.author_association == 'MEMBER' ||
        github.event.comment.author_association == 'COLLABORATOR')))
    permissions:
      contents: 'read'
      id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
      issues: 'read'
      statuses: 'read'
      packages: 'read'
    timeout-minutes: 20
    runs-on: 'ubuntu-latest'
    outputs:
      duplicate_issues_csv: '${{ env.DUPLICATE_ISSUES_CSV }}'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Log in to GitHub Container Registry'
        uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
        with:
          registry: 'ghcr.io'
          username: '${{ github.actor }}'
          password: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Find Duplicate Issues'
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_issue_deduplication'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ISSUE_TITLE: '${{ github.event.issue.title }}'
          ISSUE_BODY: '${{ github.event.issue.body }}'
          ISSUE_NUMBER: '${{ github.event.issue.number }}'
          REPOSITORY: '${{ github.repository }}'
          FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
        with:
          gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
          gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
          gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
          gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "mcpServers": {
                "issue_deduplication": {
                  "command": "docker",
                  "args": [
                    "run",
                    "-i",
                    "--rm",
                    "--network", "host",
                    "-e", "GITHUB_TOKEN",
                    "-e", "GEMINI_API_KEY",
                    "-e", "DATABASE_TYPE",
                    "-e", "FIRESTORE_DATABASE_ID",
                    "-e", "GCP_PROJECT",
                    "-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
                    "-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
                    "ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
                  ],
                  "env": {
                    "GITHUB_TOKEN": "${GITHUB_TOKEN}",
                    "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
                    "DATABASE_TYPE":"firestore",
                    "GCP_PROJECT": "${FIRESTORE_PROJECT}",
                    "FIRESTORE_DATABASE_ID": "(default)",
                    "GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
                  },
                  "enabled": true,
                  "timeout": 600000
                }
              },
              "maxSessionTurns": 25,
              "coreTools": [
                "run_shell_command(echo)",
                "run_shell_command(gh issue view)"
              ],
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              }
            }
          prompt: |-
            ## Role
            You are an issue de-duplication assistant. Your goal is to find
            duplicate issues for a given issue.
            ## Steps
            1.  **Find Potential Duplicates:**
                - The repository is ${{ github.repository }} and the issue number is ${{ github.event.issue.number }}.
                - Use the `duplicates` tool with the `repo` and `issue_number` to find potential duplicates for the current issue. Do not use the `threshold` parameter.
                - If no duplicates are found, you are done.
                - Print the JSON output from the `duplicates` tool to the logs.
            2.  **Refine Duplicates List (if necessary):**
                - If the `duplicates` tool returns between 1 and 14 results, you must refine the list.
                - For each potential duplicate issue, run `gh issue view <issue-number> --json title,body,comments` to fetch its content.
                - Also fetch the content of the original issue: `gh issue view "${ISSUE_NUMBER}" --json title,body,comments`.
                - Carefully analyze the content (title, body, comments) of the original issue and all potential duplicates.
                - It is very important if the comments on either issue mention that they are not duplicates of each other, to treat them as not duplicates.
                - Based on your analysis, create a final list containing only the issues you are highly confident are actual duplicates.
                - If your final list is empty, you are done.
                - Print to the logs if you omitted any potential duplicates based on your analysis.
                - If the `duplicates` tool returned 15+ results, use the top 15 matches (based on descending similarity score value) to perform this step.
            3.  **Output final duplicates list as CSV:**
                - Convert the list of appropriate duplicate issue numbers into a comma-separated list (CSV). If there are no appropriate duplicates, use the empty string.
                - Use the "echo" shell command to append the CSV of issue numbers into the filepath referenced by the environment variable "${GITHUB_ENV}":
                  echo "DUPLICATE_ISSUES_CSV=[DUPLICATE_ISSUES_AS_CSV]" >> "${GITHUB_ENV}"
            ## Guidelines
            - Only use the `duplicates` and `run_shell_command` tools.
            - The `run_shell_command` tool can be used with `gh issue view`.
            - Do not download or read media files like images, videos, or links. The `--json` flag for `gh issue view` will prevent this.
            - Do not modify the issue content or status.
            - Do not add comments or labels.
            - Reference all shell variables as "${VAR}" (with quotes and braces).

  add-comment-and-label:
    needs: 'find-duplicates'
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
      needs.find-duplicates.outputs.duplicate_issues_csv != '' &&
      (
       github.event_name == 'issues' ||
       github.event_name == 'workflow_dispatch' ||
       (
        github.event_name == 'issue_comment' &&
        contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
        (
         github.event.comment.author_association == 'OWNER' ||
         github.event.comment.author_association == 'MEMBER' ||
         github.event.comment.author_association == 'COLLABORATOR'
        )
       )
      )
    permissions:
      issues: 'write'
    timeout-minutes: 5
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'

      - name: 'Comment and Label Duplicate Issue'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        env:
          DUPLICATES_OUTPUT: '${{ needs.find-duplicates.outputs.duplicate_issues_csv }}'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            const rawCsv = process.env.DUPLICATES_OUTPUT;
            core.info(`Raw duplicates CSV: ${rawCsv}`);
            const duplicateIssues = rawCsv.split(',').map(s => s.trim()).filter(s => s);

            if (duplicateIssues.length === 0) {
              core.info('No duplicate issues found. Nothing to do.');
              return;
            }

            const issueNumber = ${{ github.event.issue.number }};

            function formatCommentBody(issues, updated = false) {
              const header = updated
                ? 'Found possible duplicate issues (updated):'
                : 'Found possible duplicate issues:';
              const issuesList = issues.map(num => `- #${num}`).join('\n');
              const footer = 'If you believe this is not a duplicate, please remove the `status/possible-duplicate` label.';
              const magicComment = '<!-- gemini-cli-deduplication -->';
              return `${header}\n\n${issuesList}\n\n${footer}\n${magicComment}`;
            }

            const newCommentBody = formatCommentBody(duplicateIssues);
            const newUpdatedCommentBody = formatCommentBody(duplicateIssues, true);

            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });

            const magicComment = '<!-- gemini-cli-deduplication -->';
            const existingComment = comments.find(comment =>
              comment.user.type === 'Bot' && comment.body.includes(magicComment)
            );

            let commentMade = false;

            if (existingComment) {
              // To check if lists are same, just compare the formatted bodies without headers.
              const existingBodyForCompare = existingComment.body.substring(existingComment.body.indexOf('- #'));
              const newBodyForCompare = newCommentBody.substring(newCommentBody.indexOf('- #'));

              if (existingBodyForCompare.trim() !== newBodyForCompare.trim()) {
                core.info(`Updating existing comment ${existingComment.id}`);
                await github.rest.issues.updateComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: existingComment.id,
                  body: newUpdatedCommentBody,
                });
                commentMade = true;
              } else {
                core.info('Existing comment is up-to-date. Nothing to do.');
              }
            } else {
              core.info('Creating new comment.');
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                body: newCommentBody,
              });
              commentMade = true;
            }

            if (commentMade) {
              core.info('Adding "status/possible-duplicate" label.');
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                labels: ['status/possible-duplicate'],
              });
            }
