name: CI · Changes

# Path-filter gate (#788): a single reusable job that classifies what a run's diff touches so
# downstream jobs can skip work they cannot be affected by. Consumers call this workflow and
# branch on its boolean outputs via `needs` + `if:` - NEVER a top-level `on: paths:` filter,
# which would leave a skipped check unreported instead of green.
#
# Safety rules:
# - Shared config (root package.json/lockfile/workspace file, root ts/eslint/prettier/tailwind
#   configs, and .github/workflows/**) counts as touching every gate, so a transitive change
#   can never skip a real regression.
# - Only pull_request runs are filtered. Every other event (push to main, workflow_dispatch)
#   forces all outputs to 'true' - the full pipeline always runs where it matters.

on:
  workflow_call:
    outputs:
      web:
        description: "'true' when the pnpm/TS workspace (apps/*, packages/*) or shared config changed"
        value: ${{ jobs.detect.outputs.web }}
      ui:
        description: "'true' when packages/* (@bloom/ui Storybook and its workspace deps) or shared config changed"
        value: ${{ jobs.detect.outputs.ui }}
      infra:
        description: "'true' when deploy assets (Helm chart, OpenTofu module, deploy scripts) or workflows changed"
        value: ${{ jobs.detect.outputs.infra }}
      docs:
        description: "'true' when the docs site (apps/docs), its rendered content (docs/), the TypeDoc-documented packages, or shared config changed"
        value: ${{ jobs.detect.outputs.docs }}
      landing:
        description: "'true' when the landing site (apps/landing), the @bloom/ui sources it compiles, or shared config changed"
        value: ${{ jobs.detect.outputs.landing }}

# The filter reads the PR's changed files via the GitHub API (no checkout), so the token
# needs pull-requests: read - callers must grant it on their calling job.
permissions:
  contents: read
  pull-requests: read

jobs:
  detect:
    name: Detect changed paths
    runs-on: bloom-arc
    # Non-PR events skip the filter step, so its outputs are empty and the `||` arm below
    # forces every gate open.
    outputs:
      web: ${{ github.event_name != 'pull_request' && 'true' || steps.filter.outputs.web }}
      ui: ${{ github.event_name != 'pull_request' && 'true' || steps.filter.outputs.ui }}
      infra: ${{ github.event_name != 'pull_request' && 'true' || steps.filter.outputs.infra }}
      docs: ${{ github.event_name != 'pull_request' && 'true' || steps.filter.outputs.docs }}
      landing: ${{ github.event_name != 'pull_request' && 'true' || steps.filter.outputs.landing }}
    steps:
      - uses: dorny/paths-filter@v3
        if: ${{ github.event_name == 'pull_request' }}
        id: filter
        with:
          # The root tsconfig/eslint/prettier/tailwind globs are defensive: today those
          # configs live in packages/config (matched by packages/** below), but a future
          # root-level config must not silently skip gates.
          filters: |
            shared: &shared
              - 'package.json'
              - 'pnpm-lock.yaml'
              - 'pnpm-workspace.yaml'
              - 'tsconfig*'
              - 'eslint*'
              - '.eslintrc*'
              - 'prettier*'
              - '.prettierrc*'
              - '.prettierignore'
              - 'tailwind*'
              - '.github/workflows/**'
            web:
              - *shared
              - 'apps/admin/**'
              # apps/landing is a workspace package too, so the root pnpm -r fan-out
              # (lint/format/typecheck/test) must run when it changes.
              - 'apps/landing/**'
              # apps/docs is a workspace package, so the root pnpm -r lint/format/typecheck
              # fan-out covers it - an apps/docs change must run the workspace suite.
              - 'apps/docs/**'
              - 'apps/server/**'
              - 'apps/web/**'
              - 'packages/**'
              # The workflow-spec instance + schema live under apps/server (relocated in
              # #830); the TS server is their runtime consumer (BLOOM_WORKFLOW_SPEC_PATH) and
              # its vitest suites load the real instance through the zod loadSpec twin - so a
              # spec edit must run the TS suite.
              - 'apps/server/workflows/**'
            ui:
              - *shared
              - 'packages/**'
            infra:
              - '.github/workflows/**'
              - 'deploy/**'
              - 'scripts/deploy/**'
            # The docs site (M45-1) renders the repo docs/ tree in place, so a docs/ content
            # edit can break the Docusaurus build without touching apps/docs. The build also
            # generates the TypeDoc API reference (M45-4) from packages/api-client and
            # packages/ui sources, so changes there must run the docs gate too.
            docs:
              - *shared
              - 'apps/docs/**'
              - 'docs/**'
              - 'packages/api-client/**'
              - 'packages/ui/**'
            # The landing site (M46-1) compiles @bloom/ui sources into its bundle and builds
            # with the @bloom/config preset/tsconfig, so changes there must run its build gate.
            landing:
              - *shared
              - 'apps/landing/**'
              - 'packages/config/**'
              - 'packages/ui/**'
