name: '🏷️ Gemini Automated Issue Triage'

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

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

defaults:
  run:
    shell: 'bash'

permissions:
  contents: 'read'
  id-token: 'write'
  issues: 'write'
  statuses: 'write'
  packages: 'read'
  actions: 'write' # Required for cancelling a workflow run

jobs:
  triage-issue:
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          (github.event_name == 'issues' || github.event_name == 'issue_comment') &&
          (github.event_name != 'issue_comment' || (
            contains(github.event.comment.body, '@gemini-cli /triage') &&
            (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
          ))
        )
      ) &&
      !contains(github.event.issue.labels.*.name, 'area/')
    timeout-minutes: 5
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Get issue data for manual trigger'
        id: 'get_issue_data'
        if: |-
          github.event_name == 'workflow_dispatch'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          script: |
            const { data: issue } = await github.rest.issues.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: ${{ github.event.inputs.issue_number }},
            });
            core.setOutput('title', issue.title);
            core.setOutput('body', issue.body);
            core.setOutput('labels', issue.labels.map(label => label.name).join(','));
            return issue;

      - name: 'Manual Trigger Pre-flight Checks'
        if: |-
          github.event_name == 'workflow_dispatch'
        env:
          ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number }}'
          LABELS: '${{ steps.get_issue_data.outputs.labels }}'
        run: |
          if echo "${LABELS}" | grep -q 'area/'; then
            echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
            exit 1
          fi

          echo "Manual triage checks passed."

      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - 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: 'Get Repository Labels'
        id: 'get_labels'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |-
            const { data: labels } = await github.rest.issues.listLabelsForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
            });
            const allowedLabels = [
              'area/agent',
              'area/enterprise',
              'area/non-interactive',
              'area/core',
              'area/security',
              'area/platform',
              'area/extensions',
              'area/unknown'
            ];
            const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
            core.setOutput('available_labels', labelNames.join(','));
            core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
            return labelNames;

      - name: 'Run Gemini Issue Analysis'
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_issue_analysis'
        env:
          GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
          ISSUE_TITLE: >-
            ${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
          ISSUE_BODY: >-
            ${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
          ISSUE_NUMBER: >-
            ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.issue_number || github.event.issue.number }}
          REPOSITORY: '${{ github.repository }}'
          AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
        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: |-
            {
              "maxSessionTurns": 25,
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              }
            }
          prompt: |-
            ## Role

            You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label based on the definitions provided.

            ## Steps
            1. Review the issue title and body: ${{ env.ISSUE_TITLE }} and ${{ env.ISSUE_BODY }}.
            2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
            3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
            4. Fallback Logic:
                - If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
            5. Output your selected label in JSON format and nothing else. Example:
                {"labels_to_set": ["area/core"]}

            ## Guidelines
            - Your output must contain exactly one area/ label.
            - Triage only the current issue based on its title and body.
            - Output only valid JSON format.
            - Do not include any explanation or additional text, just the JSON.

            Reference 1: Area Definitions
            area/agent
            - Description: Issues related to the "brain" of the CLI. This includes the core agent logic, model quality, tool/function calling, and memory.
            - Example Issues:
              "I am not getting a reasonable or expected response."
              "The model is not calling the tool I expected."
              "The web search tool is not working as expected."
              "Feature request for a new built-in tool (e.g., read file, write file)."
              "The generated code is poor quality or incorrect."
              "The model seems stuck in a loop."
              "The response from the model is malformed (e.g., broken JSON, bad formatting)."
              "Concerns about unnecessary token consumption."
              "Issues with how memory or chat history is managed."
              "Issues with sub-agents."
              "Model is switching from one to another unexpectedly."

            area/enterprise
            - Description: Issues specific to enterprise-level features, including telemetry, policy, and licenses.
            - Example Issues:
              "Usage data is not appearing in our telemetry dashboard."
              "A user is able to perform an action that should be blocked by an admin policy."
              "Questions about billing, licensing tiers, or enterprise quotas."

            area/non-interactive
            - Description: Issues related to using the CLI in automated or non-interactive environments (headless mode).
            - Example Issues:
              "Problems using the CLI as an SDK in another surface."
              "The CLI is behaving differently when run from a shell script vs. an interactive terminal."
              "GitHub action is failing."
              "I am having trouble running the CLI in headless mode"

            area/core
            - Description: Issues with the fundamental CLI app itself. This includes the user interface (UI/UX), installation, OS compatibility, and performance.
            - Example Issues:
              "I am seeing my screen flicker when using the CLI."
              "The output in my terminal is malformed or unreadable."
              "Theme changes are not taking effect."
              "Keyboard inputs (e.g., arrow keys, Ctrl+C) are not being recognized."
              "The CLI failed to install or update."
              "An issue specific to running on Windows, macOS, or Linux."
              "Problems with command parsing, flags, or argument handling."
              "High CPU or memory usage by the CLI process."
              "Issues related to multi-modality (e.g., handling image inputs)."
              "Problems with the IDE integration connection or installation"

            area/security
            - Description: Issues related to user authentication, authorization, data security, and privacy.
            - Example Issues:
              "I am unable to sign in."
              "The login flow is selecting the wrong authentication path"
              "Problems with API key handling or credential storage."
              "A report of a security vulnerability"
              "Concerns about data sanitization or potential data leaks."
              "Issues or requests related to privacy controls."
              "Preventing unauthorized data access."

            area/platform
            - Description: Issues related to CI/CD, release management, testing, eval infrastructure, capacity, quota management, and sandbox environments.
            - Example Issues:
              "I am getting a 429 'Resource Exhausted' or 500-level server error."
              "General slowness or high latency from the service."
              "The build script is broken on the main branch."
              "Tests are failing in the CI/CD pipeline."
              "Issues with the release management or publishing process."
              "User is running out of capacity."
              "Problems specific to the sandbox or staging environments."
              "Questions about quota limits or requests for increases."

            area/extensions
            - Description: Issues related to the extension ecosystem, including the marketplace and website.
            - Example Issues:
              "Bugs related to the extension marketplace website."
              "Issues with a specific extension."
              "Feature request for the extension ecosystem."

            area/unknown
            - Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.

      - name: 'Apply Labels to Issue'
        if: |-
          ${{ steps.gemini_issue_analysis.outputs.summary != '' }}
        env:
          REPOSITORY: '${{ github.repository }}'
          ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
          LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |
            const rawOutput = process.env.LABELS_OUTPUT;
            core.info(`Raw output from model: ${rawOutput}`);
            let parsedLabels;
            try {
              // First, try to parse the raw output as JSON.
              parsedLabels = JSON.parse(rawOutput);
            } catch (jsonError) {
              // If that fails, check for a markdown code block.
              core.warning(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
              const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
              if (jsonMatch && jsonMatch[1]) {
                try {
                  parsedLabels = JSON.parse(jsonMatch[1].trim());
                } catch (markdownError) {
                  core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawOutput}`);
                  return;
                }
              } else {
                core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawOutput}`);
                return;
              }
            }

            const issueNumber = parseInt(process.env.ISSUE_NUMBER);
            const labelsToAdd = parsedLabels.labels_to_set || [];

            if (labelsToAdd.length !== 1) {
              core.setFailed(`Expected exactly 1 label (area/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
              return;
            }

            // Set labels based on triage result
            await github.rest.issues.addLabels({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
              labels: labelsToAdd
            });
            core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`);

            // Remove the 'status/need-triage' label
            try {
                await github.rest.issues.removeLabel({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issueNumber,
                    name: 'status/need-triage'
                });
                core.info(`Successfully removed 'status/need-triage' label.`);
            } catch (error) {
                // If the label doesn't exist, the API call will throw a 404. We can ignore this.
                if (error.status !== 404) {
                    core.warning(`Failed to remove 'status/need-triage': ${error.message}`);
                }
            }

      - name: 'Post Issue Analysis Failure Comment'
        if: |-
          ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
        env:
          ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
          RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |-
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: parseInt(process.env.ISSUE_NUMBER),
              body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
            })
