name: Breaking changes review

# Runs Claude against the PR diff, looking for changes that could break
# consumers of the published `@rootplatform/cli` npm package — CI pipelines
# scripting `rp` commands, cloned product-module workspaces relying on
# generated files, flag/output/exit-code contracts, and API payload shapes.
#
# Verdict shape mirrors security-review:
#   green  → no breaking-change risk
#   yellow → advisory, won't block (worth a glance)
#   red    → high-confidence breaking change; blocks merge via branch protection
#
# Ported from root-platform's breaking-changes-review.yml, retuned for a
# published CLI (no DB migrations / rolling deploys here — the axis is
# semver + backwards compatibility for existing users and workspaces).
# Uses Claude Max / Pro via OAuth token (CLAUDE_CODE_OAUTH_TOKEN secret).

on:
  pull_request:
    types: [opened, ready_for_review]
  issue_comment:
    types: [created]

permissions:
  contents: read
  pull-requests: write
  issues: write
  statuses: write

concurrency:
  group: breaking-changes-review-${{ github.event.pull_request.number || github.event.issue.number }}
  cancel-in-progress: true

jobs:
  breaking-changes-review:
    # Auto-run once on PR open or draft->ready transition. Re-run on demand
    # when someone comments `/review` (all 4) or `/review breaking` (just this one).
    if: |
      (github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
      (github.event_name == 'issue_comment' && github.event.issue.pull_request != null &&
       startsWith(github.event.comment.body, '/review') &&
       !startsWith(github.event.comment.body, '/review security') &&
       !startsWith(github.event.comment.body, '/review coverage') &&
       !startsWith(github.event.comment.body, '/review quality') &&
       !startsWith(github.event.comment.body, '/review-help'))
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Resolve PR context
        id: ctx
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # PR number + head SHA come from different event payloads.
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            PR=${{ github.event.pull_request.number }}
            SHA=${{ github.event.pull_request.head.sha }}
          else
            PR=${{ github.event.issue.number }}
            SHA=$(gh api repos/${{ github.repository }}/pulls/$PR --jq .head.sha)
          fi
          echo "pr_number=$PR" >> "$GITHUB_OUTPUT"
          echo "head_sha=$SHA" >> "$GITHUB_OUTPUT"
          echo "Resolved PR=$PR SHA=$SHA"

      - name: React to trigger comment
        if: github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh api -X POST \
            repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
            -f content=eyes >/dev/null || true

      - name: Checkout PR head
        uses: actions/checkout@v6
        with:
          ref: ${{ steps.ctx.outputs.head_sha }}
          fetch-depth: 0

      - name: Capture PR diff + relevant rules
        id: pr
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ steps.ctx.outputs.pr_number }}
        run: |
          gh pr diff "$PR_NUMBER" > /tmp/pr.diff
          diff_size=$(wc -c < /tmp/pr.diff)
          echo "diff_size=$diff_size" >> "$GITHUB_OUTPUT"
          echo "diff_lines=$(wc -l < /tmp/pr.diff)" >> "$GITHUB_OUTPUT"
          mkdir -p /tmp/ctx
          {
            for f in \
              CLAUDE.md \
              .cursor/rules/cli-messaging.mdc; do
              if [ -f "$f" ]; then
                echo "================================================================"
                echo "FILE: $f"
                echo "================================================================"
                cat "$f"
                echo
              fi
            done
          } > /tmp/ctx/rules.md
          echo '--- Diff preview (first 100 lines) ---'
          head -100 /tmp/pr.diff || true

      - name: Run Claude breaking-changes review
        id: review
        uses: anthropics/claude-code-base-action@v0.0.63
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          model: 'claude-opus-4-7'
          allowed_tools: 'Read,Grep,Bash'
          max_turns: '35'
          timeout_minutes: '12'
          prompt: |
            You are conducting a BREAKING-CHANGES review of a pull request
            to root-platform-cli — the Root Platform Workbench CLI (`rp`),
            published to npm as `@rootplatform/cli`. Consumers are: CI
            pipelines scripting rp commands (Makefiles, GitHub workflows),
            developers with cloned product-module workspaces containing
            CLI-generated files, and automation parsing CLI output or
            exit codes. Your job: find changes that could break any of
            them when they upgrade.

            False negatives (silent breakages that ship) are the failure
            mode you're guarding against. There is no rolling deploy here —
            the hazard is the npm upgrade boundary: every existing user,
            script, and workspace created by an OLDER version must keep
            working with the NEW version (or the break must be flagged for
            a semver-major/minor + release note).

            REPOSITORY: ${{ github.repository }}
            PR: #${{ github.event.pull_request.number }} — "${{ github.event.pull_request.title }}"
            DIFF SIZE: ${{ steps.pr.outputs.diff_size }} bytes, ${{ steps.pr.outputs.diff_lines }} lines

            ============================================================
            !!  CRITICAL: EMIT THE VERDICT JSON OR THE RUN IS WASTED
            ============================================================
            Your run has a HARD 35-turn ceiling. If you do not emit the
            ::review_json:: block before that ceiling, the workflow falls
            back to a placeholder verdict and your compute is discarded.
            A partial / yellow verdict is ALWAYS better than no verdict.
            Plan your tool calls accordingly.

            ============================================================
            OUTPUT FORMAT — READ THIS FIRST AND DO NOT FORGET IT
            ============================================================
            Your FINAL assistant message MUST contain exactly one JSON
            object wrapped in the markers below, on lines by themselves
            with NO surrounding code fences (no ``` and no ~~~), and NO
            commentary after the closing marker. The workflow parses
            these markers; without them the entire audit is wasted.

            ::review_json::
            {
              "verdict": "red" | "yellow" | "green",
              "summary": "<one-sentence overall assessment>",
              "findings": [
                {
                  "severity": "high" | "medium" | "low" | "info",
                  "category": "flag-or-command-removed" | "exit-code-change" | "output-format-break" | "workspace-format-change" | "api-payload-change" | "config-file-change" | "default-value-flip" | "node-or-dependency-floor" | "removed-export-still-referenced",
                  "location": "<file path:line, or 'general' if cross-cutting>",
                  "description": "<what could break and which consumer/workflow is affected>",
                  "fix": "<concrete suggestion — e.g. 'keep the old flag as an alias', 'flag for semver-major + release note', 'add a migration path on pull'>"
                }
              ]
            }
            ::end::

            Empty `findings: []` is fine. A yellow verdict with partial
            confidence is FAR better than no verdict at all.

            ============================================================
            TURN BUDGET — HARD CAP
            ============================================================
            Hard limit: 12 tool-calling turns before you MUST emit.
              - Turns 1-2: Read /tmp/pr.diff + /tmp/ctx/rules.md.
              - Turns 3-10: Read FULL files for any change to command
                registration (src/index.ts), exit paths, definition
                read/write helpers, or API payload construction. For
                removed/renamed exports, flags, or config keys: grep for
                remaining references across src/ and generated-file
                templates.
              - Turn 11: STOP exploring. Turn 12: EMIT.

            ============================================================

            You have Read, Grep, and Bash (git, rg, cat, head, tail) tools.
            The PR diff is in /tmp/pr.diff. Project conventions are in
            /tmp/ctx/rules.md.

            BREAKING-CHANGE CATEGORIES — scan for each:

              1. **flag-or-command-removed** — a command, flag, or alias in
                 src/index.ts is removed or renamed (e.g. `-f`, `--force`,
                 `--no-sort`). Scripts invoking the old spelling break.
                 Renames need the old form kept as an alias or a
                 semver-major with release notes.

              2. **exit-code-change** — a path's exit code changes (0→1 or
                 1→0), or a previously-throwing path now returns. CI
                 pipelines branch on exit codes; any deliberate change
                 (like a fixed silent-success bug) must be flagged in the
                 release description and usually warrants a minor/major
                 bump. Verify the change is intentional and documented in
                 the PR — if so, note it as info, not a blocker.

              3. **output-format-break** — machine-consumed output changes
                 shape: JSON printed by commands (invoke --verbose, logs),
                 lines that scripts grep (success lines, "Aborting"
                 lines), or file outputs. Human-facing wording tweaks are
                 fine; structural changes to parseable output are not.

              4. **workspace-format-change** — the on-disk layout or file
                 contents the CLI reads/writes in cloned workspaces
                 changes incompatibly: .root-config / root_config shape,
                 documents/ layout, code file ordering, generated static
                 files, skill files. An OLD workspace pulled/pushed with
                 the NEW CLI must still round-trip. Check the
                 write-and-read test suites moved with the change.

              5. **api-payload-change** — the shape the CLI sends to or
                 expects from the Root Platform API changes (push payloads,
                 definition network mappers `productModuleDefinitionTo/
                 FromNetwork`). Must stay compatible with the deployed
                 platform API — cross-check whether a platform-side PR is
                 referenced as a dependency.

              6. **config-file-change** — a key in the auth/config files
                 the CLI reads (~/.root auth, .root-config, jsconfig
                 helpers) is renamed or gains a required field without a
                 fallback for files written by older versions.

              7. **default-value-flip** — a flag default, host default, or
                 behaviour default flips (e.g. sort on by default,
                 different default environment). Existing invocations see
                 different behaviour without changing themselves.

              8. **node-or-dependency-floor** — engines/.nvmrc bumps, or a
                 dependency change that raises the minimum Node version.
                 Users on older Node get broken installs; needs a major
                 bump + release note.

              9. **removed-export-still-referenced** — a module removes an
                 export that's still imported elsewhere in src/ or
                 referenced by generated-file templates. Grep for the
                 symbol; a stale reference breaks the build or, worse,
                 users' generated workspaces.

            SCOPING:
              - Only flag risks introduced by this PR. Pre-existing risk
                unrelated to the diff is noise.
              - Deliberate, documented behaviour changes (called out in the
                PR/release description) are INFO — confirm the semver
                implication is acknowledged, don't block.
              - Refactor PRs that secretly change behaviour ("just renaming
                X to Y") are the hardest to spot — read the FULL file, not
                just the diff hunks.

            VERDICT — choose ONE based on the WORST severity in findings:
              - "red"    — at least one HIGH severity breaking-change risk
                           that is NOT acknowledged in the PR. Blocks merge.
              - "yellow" — only MEDIUM, LOW, or INFO. Advisory; doesn't block.
              - "green"  — no breaking-change risk. Clean pass.

            SEVERITY GUIDELINES:
              - HIGH: an existing script, workspace, or API interaction is
                near-certain to break on upgrade, with the consumer
                identified, and the PR doesn't acknowledge it.
              - MEDIUM: plausible breakage under specific conditions (a
                particular flag combination, an old workspace shape).
              - LOW: a deprecated path still works but is now inconsistent
                with the new canonical one.
              - INFO: acknowledged/deliberate behaviour change; internal
                refactor no current consumer depends on.

            REMINDER — by turn 11 STOP, by turn 12 EMIT. The markers must
            be on their own lines with no code fences and no commentary
            after `::end::`.

      - name: Parse verdict, post comment, set status
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ steps.ctx.outputs.pr_number }}
          HEAD_SHA: ${{ steps.ctx.outputs.head_sha }}
          REPO: ${{ github.repository }}
          RUN_ID: ${{ github.run_id }}
        run: |
          python3 .github/scripts/post-claude-review.py \
            --context "Breaking changes review" \
            --review-kind "breaking-changes" \
            --execution-file "${{ steps.review.outputs.execution_file }}" \
            --workflow-file ".github/workflows/breaking-changes-review.yml" \
            --findings-heading "Breaking-change risks"

      - name: Delete trigger comment (issue_comment only)
        # When the workflow was fired by a /review comment, delete that
        # comment so the timeline doesn't fill up with trigger noise.
        # The verdict sticky comment is the persistent artifact.
        if: always() && github.event_name == 'issue_comment'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh api -X DELETE \
            "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}" \
            >/dev/null 2>&1 || true
