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'
  workflow_call:
    inputs:
      issue_number:
        description: 'issue number to triage'
        required: false
        type: 'string'

concurrency:
  group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || 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.repository == 'google-gemini/maintainers-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 issueNumber = ${{ github.event.inputs.issue_number || inputs.issue_number }};
            const { data: issue } = await github.rest.issues.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });
            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 || 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
        with:
          persist-credentials: false

      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        env:
          APP_ID: '${{ secrets.APP_ID }}'
        if: |-
          ${{ env.APP_ID != '' }}
        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 || secrets.GITHUB_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/documentation',
              '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: 'Prepare Issue Data'
        id: 'prepare_issue_data'
        env:
          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 }}
        run: |
          set -euo pipefail
          echo "Title: ${ISSUE_TITLE}" > issue_context.md
          echo "Body:" >> issue_context.md
          echo "${ISSUE_BODY}" >> issue_context.md

      - 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_NUMBER: >-
            ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.issue_number || inputs.issue_number) || github.event.issue.number }}
          REPOSITORY: '${{ github.repository }}'
          AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
          GEMINI_CLI_TRUST_WORKSPACE: 'true'
        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"
              },
              "coreTools": [
                "run_shell_command(echo)",
                "read_file"
              ]
            }
          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. Use the read_file tool to read the file "issue_context.md" which contains the issue title and 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/documentation
            - Description: Issues related to user-facing documentation and other content on the documentation website.
            - Example Issues:
              "A typo in a README file."
              "DOCS: A command is not working as described in the documentation."
              "A request for a new documentation page."
              "Instructions missing for skills feature"

            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 || secrets.GITHUB_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 {
                // If no markdown block, try to find a raw JSON object in the output.
                // The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
                // before the actual JSON response.
                const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
                if (jsonObjectMatch) {
                  try {
                    parsedLabels = JSON.parse(jsonObjectMatch[0]);
                  } catch (extractError) {
                    core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
                    return;
                  }
                } else {
                  core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\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;
            }

            const newAreaLabel = labelsToAdd[0];

            // Get current labels to resolve conflicts
            const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });
            const currentLabelNames = currentLabels.map(l => l.name);
            const currentAreaLabels = currentLabelNames.filter(name => name.startsWith('area/'));

            const labelsToRemove = currentAreaLabels.filter(name => name !== newAreaLabel);

            for (const label of labelsToRemove) {
              try {
                await github.rest.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issueNumber,
                  name: label
                });
                core.info(`Removed conflicting area label: ${label}`);
              } catch (e) {
                core.warning(`Failed to remove label ${label}: ${e.message}`);
              }
            }

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

      - 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 || secrets.GITHUB_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.'
            })
