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_json: '${{ steps.gemini_issue_deduplication.outputs.summary }}' 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 --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 JSON:** - Output the final list of duplicate issue numbers in a JSON format. - For example: `{"duplicate_issues": [123, 456]}` - If no duplicates are found, output `{"duplicate_issues": []}`. - Do not include any explanation or additional text, just the JSON. ## 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_json && (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_json }}' with: github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' script: |- const rawJson = process.env.DUPLICATES_OUTPUT; core.info(`Raw duplicates JSON: ${rawJson}`); let parsedJson; try { const trimmedJson = rawJson.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, '').trim(); parsedJson = JSON.parse(trimmedJson); core.info(`Parsed duplicates JSON: ${JSON.stringify(parsedJson)}`); } catch (err) { core.setFailed(`Failed to parse duplicates JSON from Gemini output: ${err.message}\nRaw output: ${rawJson}`); return; } if (!parsedJson.duplicate_issues || parsedJson.duplicate_issues.length === 0) { core.info('No duplicate issues found. Nothing to do.'); return; } const issueNumber = ${{ github.event.issue.number }}; const duplicateIssues = parsedJson.duplicate_issues; 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 = ''; 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 = ''; 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'], }); }