# .github/workflows/agent-api-check.yml
#
# Detects breaking changes to the AgentTool ACTION contract - the actions an
# agent dispatches and the parameters each action accepts - and posts a comment
# on the PR. This is the surface most users of the SDK build against, and it is
# NOT the Python signature surface the griffe breaking-change check covers: a
# rename like add_camera(name=) -> add_camera(camera_name=) breaks agent code
# without changing any public Python signature griffe would flag.
#
# The check builds a {action: [params]} snapshot from the simulation backend's
# dispatcher (see .github/scripts/agent_api_snapshot.py), for both the base
# branch and the PR, and diffs them. Because it reads _ACTION_ALIASES and method
# signatures, it must IMPORT the backend, so it installs the sim-mujoco extra
# (CPU-only) and renders headless with osmesa.
#
# INFORMATIONAL ONLY. It never blocks a merge - the signal is the PR comment.
#
# Actions are pinned to full commit SHAs per the AGENTS.md Action Pinning
# convention (a mutable tag can be repointed to malicious code).

name: Agent API Check

on:
  pull_request:
    branches: [main]
    paths:
      - 'strands_robots/simulation/**'
      - '.github/scripts/agent_api_snapshot.py'
      - '.github/workflows/agent-api-check.yml'

permissions:
  contents: read       # checkout
  pull-requests: write # post/update PR comment

jobs:
  check-agent-api:
    name: Detect AgentTool API Breaking Changes
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout PR
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Python
        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065  # v5.6.0
        with:
          python-version: '3.12'
          cache: 'pip'

      - name: Install system dependencies (OpenGL for MuJoCo)
        run: |
          sudo apt-get update
          sudo apt-get install -y libosmesa6-dev

      # Importing the sim backend needs the sim-mujoco extra. CPU-only backend,
      # headless osmesa rendering - no GPU required.
      - name: Install strands-robots (sim-mujoco)
        env:
          UV_TORCH_BACKEND: cpu
        run: pip install --no-cache-dir -e ".[sim-mujoco]"

      # Snapshot the PR's action contract.
      - name: Snapshot PR contract
        env:
          MUJOCO_GL: osmesa
        run: python .github/scripts/agent_api_snapshot.py snapshot /tmp/api_pr.json

      # Snapshot the base branch's contract. Check out the base worktree, install
      # it, and run the SAME script from the PR (the base branch may not carry the
      # script yet, so copy the PR's copy in). If the base cannot be snapshotted
      # (e.g. the script is new), emit a neutral note instead of failing.
      - name: Snapshot base contract
        id: base
        env:
          MUJOCO_GL: osmesa
          BASE_REF: ${{ github.base_ref }}
        run: |
          set +e
          git worktree add /tmp/base "origin/${BASE_REF}" 2>/tmp/wt.err
          if [ $? -ne 0 ]; then
            echo "skip=true" >> "$GITHUB_OUTPUT"
            echo "::warning::could not create base worktree: $(cat /tmp/wt.err)"
            exit 0
          fi
          # Use the PR's script + PR's install (contract is read from the checked-out
          # base source tree, so run the script with /tmp/base as CWD).
          cp .github/scripts/agent_api_snapshot.py /tmp/base/.github/scripts/agent_api_snapshot.py 2>/dev/null || true
          ( cd /tmp/base && python "${GITHUB_WORKSPACE}/.github/scripts/agent_api_snapshot.py" snapshot /tmp/api_base.json )
          if [ ! -f /tmp/api_base.json ]; then
            echo "skip=true" >> "$GITHUB_OUTPUT"
            echo "::warning::base snapshot not produced; skipping diff"
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Diff contracts
        id: diff
        if: steps.base.outputs.skip != 'true'
        run: |
          set +e
          python .github/scripts/agent_api_snapshot.py diff /tmp/api_base.json /tmp/api_pr.json
          echo "exit_code=$?" >> "$GITHUB_OUTPUT"

      - name: Neutral note when base skipped
        if: steps.base.outputs.skip == 'true'
        run: |
          printf '%s\n' \
            "## AgentTool API Check Skipped" "" \
            "The base branch contract could not be snapshotted (usually because this check is new on the base). It will run normally once merged." \
            > /tmp/agent-api-changes.md

      # Always write the report to the job summary. This needs no token
      # permissions and works on fork PRs (where GITHUB_TOKEN is read-only), so
      # the signal is never lost even if the PR comment cannot be posted.
      - name: Write job summary
        if: always()
        run: |
          if [ -f /tmp/agent-api-changes.md ]; then
            cat /tmp/agent-api-changes.md >> "$GITHUB_STEP_SUMMARY"
          else
            printf '%s\n' "## AgentTool API Check" "" "No report produced." >> "$GITHUB_STEP_SUMMARY"
          fi

      # Posting a PR comment needs issues:write, which GitHub forces to
      # read-only for pull_request events from forks. This step is therefore
      # best-effort: a 403 is logged as a notice and does not fail the job. The
      # authoritative report is the job summary above. We do NOT switch to
      # pull_request_target, which would run untrusted PR code (this workflow
      # installs and imports the branch) with a write token.
      - name: Post or update PR comment (best-effort)
        if: always()
        continue-on-error: true
        uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b  # v7.1.0
        env:
          DIFF_EXIT: ${{ steps.diff.outputs.exit_code }}
        with:
          script: |
            const fs = require('fs');
            let body = '';
            try {
              body = fs.readFileSync('/tmp/agent-api-changes.md', 'utf8');
            } catch (e) {
              body = '## AgentTool API Check\n\nNo report produced.';
            }
            const marker = '<!-- agent-api-check -->';
            const fullBody = `${marker}\n${body}`;

            try {
              const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.payload.pull_request.number,
              });
              const existing = comments.find(c => c.body.includes(marker));
              if (existing) {
                await github.rest.issues.updateComment({
                  owner: context.repo.owner, repo: context.repo.repo,
                  comment_id: existing.id, body: fullBody,
                });
              } else {
                await github.rest.issues.createComment({
                  owner: context.repo.owner, repo: context.repo.repo,
                  issue_number: context.payload.pull_request.number, body: fullBody,
                });
              }
            } catch (e) {
              // Expected on fork PRs: GITHUB_TOKEN is read-only, so commenting
              // returns 403. The job summary carries the report; do not fail.
              core.notice(`Could not post PR comment (${e.status || 'error'}); see the job summary for the AgentTool API report.`);
            }
            if (process.env.DIFF_EXIT === '1') {
              core.warning('AgentTool breaking changes detected - see the job summary / PR comment.');
            }
