# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Reusable workflow: generate Allure reports and deploy to GitHub Pages.
# Called by pr.yaml (pre-merge) and allure-report.yml (post-merge/nightly/release).

name: Generate Allure Report

on:
  workflow_call:
    inputs:
      run_id:
        description: 'Workflow run ID to download allure-results artifacts from'
        required: true
        type: string
      subdir:
        description: 'Subdirectory under allure/ on gh-pages (e.g. pr, post-merge, nightly, release)'
        required: true
        type: string
      workflow_label:
        description: 'Workflow label injected into allure results for dashboard filtering'
        required: true
        type: string

permissions:
  contents: write
  actions: read

jobs:
  generate-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2

      - name: Download Allure Results
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093  # v4.3.0
        with:
          run-id: ${{ inputs.run_id }}
          pattern: allure-results-*
          path: allure-results
          merge-multiple: true
          github-token: ${{ github.token }}

      - name: Check for Allure results
        id: check-results
        run: |
          if [ -d allure-results ] && [ "$(ls -A allure-results 2>/dev/null)" ]; then
            echo "has_results=true" >> $GITHUB_OUTPUT
            echo "Found allure results:"
            ls -la allure-results/
          else
            echo "has_results=false" >> $GITHUB_OUTPUT
            echo "No allure results found. Skipping report generation."
          fi

      - name: Inject workflow label and CI link into allure results
        if: steps.check-results.outputs.has_results == 'true'
        env:
          WORKFLOW_LABEL: ${{ inputs.workflow_label }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ inputs.run_id }}
        run: |
          # Inject dynamo_workflow label (for dashboard filtering) and CI run link
          # (for tracing test failures to the originating workflow run) into all
          # result files. Uses the triggering run_id, not the generating run,
          # so imported results in the unified dashboard link to the correct run.
          python3 -c "
          import json, glob, sys
          wf, run_url = sys.argv[1], sys.argv[2]
          for f in glob.glob('allure-results/*-result.json'):
              with open(f, 'r') as fh:
                  data = json.load(fh)
              labels = data.setdefault('labels', [])
              if not any(l.get('name') == 'dynamo_workflow' for l in labels):
                  labels.append({'name': 'dynamo_workflow', 'value': wf})
              links = data.setdefault('links', [])
              if not any(l.get('name') == 'CI Run' for l in links):
                  links.append({'name': 'CI Run', 'url': run_url, 'type': 'custom'})
              with open(f, 'w') as fh:
                  json.dump(data, fh)
          " "$WORKFLOW_LABEL" "$RUN_URL"

      - name: Redact secrets from allure results
        if: steps.check-results.outputs.has_results == 'true'
        run: |
          # Defense-in-depth: strip HF tokens that may leak via pytest tracebacks
          # (--showlocals dumps local variable values into allure statusDetails.trace).
          # The primary fix is inlining env var lookups in tests/conftest.py; this
          # catches any tokens that slip through via other code paths or log attachments.
          # Uses binary mode to safely handle both text and binary attachments.
          python3 -c "
          import re, glob
          pattern = re.compile(rb'hf_[A-Za-z0-9]{20,}')
          replacement = b'<HF_TOKEN_REDACTED>'
          for f in glob.glob('allure-results/*-result.json') + glob.glob('allure-results/*-attachment.*'):
              with open(f, 'rb') as fh:
                  content = fh.read()
              sanitized = pattern.sub(replacement, content)
              if sanitized != content:
                  with open(f, 'wb') as fh:
                      fh.write(sanitized)
                  print(f'Redacted token in {f}')
          "

      - name: Install Allure 2 CLI
        if: steps.check-results.outputs.has_results == 'true'
        run: |
          ALLURE_VERSION="2.38.0"
          wget -q "https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.tgz"
          tar -xzf "allure-${ALLURE_VERSION}.tgz"
          mv "allure-${ALLURE_VERSION}" allure2-cli

      - name: Install Allure 3 CLI
        if: steps.check-results.outputs.has_results == 'true'
        run: |
          npm install allure@3.3.1

      - name: Get Allure history from gh-pages
        if: steps.check-results.outputs.has_results == 'true'
        env:
          SUBDIR: ${{ inputs.subdir }}
        run: |
          git fetch --depth=1 origin gh-pages:gh-pages 2>/dev/null || echo "No gh-pages branch yet"

          # Restore history for Allure 2 report (at allure/v2/${SUBDIR}/)
          if git show "gh-pages:allure/v2/${SUBDIR}/history" 2>/dev/null; then
            mkdir -p allure-results/history
            for f in history.json history-trend.json duration-trend.json categories-trend.json retry-trend.json; do
              git show "gh-pages:allure/v2/${SUBDIR}/history/$f" > "allure-results/history/$f" 2>/dev/null || true
            done
            echo "Restored Allure 2 history from gh-pages/allure/v2/${SUBDIR}"
          else
            echo "No previous Allure 2 history found"
          fi

      - name: Generate Allure 2 Report
        if: steps.check-results.outputs.has_results == 'true'
        run: |
          ./allure2-cli/bin/allure generate allure-results -o allure-report --clean

      - name: Generate unified dashboard
        if: steps.check-results.outputs.has_results == 'true'
        env:
          SUBDIR: ${{ inputs.subdir }}
        run: |
          # Combine this workflow's results with stored results from other workflows
          mkdir -p unified-workspace/allure-results

          # Copy this workflow's results (exclude Allure 2 history dir)
          rsync -a --exclude='history' allure-results/ unified-workspace/allure-results/

          # Fetch stored results from other workflows on gh-pages (redundant if already fetched above, harmless)
          git fetch --depth=1 origin gh-pages:gh-pages 2>/dev/null || true
          for dir in pr post-merge nightly release; do
            if [ "$dir" = "$SUBDIR" ]; then continue; fi
            if git show "gh-pages:dashboard-results/${dir}" 2>/dev/null; then
              git archive gh-pages "dashboard-results/${dir}" | tar -x -C /tmp/
              cp -r "/tmp/dashboard-results/${dir}/"* "unified-workspace/allure-results/" 2>/dev/null || true
            fi
          done

          # Restore unified dashboard history for trend charts.
          # historyPath in allurerc.mjs resolves relative to process.cwd(), not --cwd,
          # so the file must be at $GITHUB_WORKSPACE/history.jsonl.
          if git show "gh-pages:allure/history.jsonl" 2>/dev/null; then
            git show "gh-pages:allure/history.jsonl" > "${GITHUB_WORKSPACE}/history.jsonl"
            echo "Restored unified dashboard history from gh-pages"
          else
            echo "No previous unified dashboard history found — Allure 3 will create it"
          fi

          # Generate unified dashboard using allurerc.mjs
          npx allure generate --config="${GITHUB_WORKSPACE}/.github/allurerc.mjs" --cwd unified-workspace -o allure-all-report

      - name: Deploy to GitHub Pages
        if: steps.check-results.outputs.has_results == 'true'
        env:
          SUBDIR: ${{ inputs.subdir }}
        run: |
          DEPLOY_DIR=$(mktemp -d)
          cd "$DEPLOY_DIR"
          git init
          git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n "x-access-token:${{ github.token }}" | base64)"

          # Apply all report files on top of the latest gh-pages state.
          # Extracted as a function so it can be re-run on push conflict.
          apply_changes() {
            if git ls-remote --exit-code origin gh-pages &>/dev/null; then
              git fetch --depth=1 origin gh-pages
              git checkout -B gh-pages FETCH_HEAD
            else
              git checkout --orphan gh-pages
              git rm -rf . 2>/dev/null || true
            fi

            # Deploy Allure 2 report
            mkdir -p "allure/v2/${SUBDIR}"
            rm -rf "allure/v2/${SUBDIR}/"*
            cp -r "${GITHUB_WORKSPACE}/allure-report/"* "allure/v2/${SUBDIR}/"

            # Store raw results for unified dashboard aggregation (exclude Allure 2 history)
            mkdir -p "dashboard-results/${SUBDIR}"
            rm -rf "dashboard-results/${SUBDIR}/"*
            rsync -a --exclude='history' "${GITHUB_WORKSPACE}/allure-results/" "dashboard-results/${SUBDIR}/"

            # Deploy unified dashboard (per-workflow tabs generated by allurerc.mjs plugins)
            # Clean plugin tabs first — each run generates fresh files with unique UUIDs,
            # and cp -r merges rather than replaces, causing unbounded file accumulation.
            rm -rf allure/pr allure/postMerge allure/nightly allure/release \
                   allure/vllm allure/sglang allure/trtllm \
                   allure/post-merge
            mkdir -p allure
            cp -r "${GITHUB_WORKSPACE}/allure-all-report/"* allure/
            # Persist history.jsonl for trend charts (written by Allure 3 at process.cwd())
            if [ -f "${GITHUB_WORKSPACE}/history.jsonl" ]; then
              cp "${GITHUB_WORKSPACE}/history.jsonl" allure/history.jsonl
            fi

            # Stage and commit
            git add -A
            if git diff --cached --quiet; then
              echo "No changes to deploy"
              return 1
            fi
            git commit -m "Deploy Allure reports for ${SUBDIR} (run ${{ github.run_id }})"
          }

          # Apply changes and push with retry on conflict.
          # Concurrent deploys can cause push rejection — retry by fetching
          # the latest gh-pages and re-applying all changes on top.
          for attempt in 1 2 3; do
            apply_changes || exit 0  # exit 0 if no changes
            if git push origin gh-pages; then
              echo "Deploy successful (attempt $attempt)"
              exit 0
            fi
            echo "Push failed (attempt $attempt/3), waiting 5s before retrying..."
            sleep 5
          done
          echo "::error::Deploy failed after 3 attempts"
          exit 1

      - name: Print report URLs
        if: steps.check-results.outputs.has_results == 'true'
        env:
          SUBDIR: ${{ inputs.subdir }}
        run: |
          echo "## Allure Reports" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "- [Allure Dashboard](https://ai-dynamo.github.io/dynamo/allure/)" >> $GITHUB_STEP_SUMMARY
          echo "- [Allure 2 Report](https://ai-dynamo.github.io/dynamo/allure/v2/${SUBDIR}/)" >> $GITHUB_STEP_SUMMARY
