---
name: Validation Pipeline

on:
  # Enable workflow as callable from another workflow
  workflow_call:
    inputs:
      python-versions-linux:
        description: 'Python versions to test on Linux (JSON array)'
        required: true
        type: string
      python-versions-windows:
        description: 'Python versions to test on Windows (JSON array)'
        required: true
        type: string
      files-changed:
        description: 'Boolean string result for if any files have changed'
        type: string
        required: false
        default: 'false'
      build-files-changed:
        description: 'Boolean string result for if build files have changed'
        type: string
        required: false
        default: 'false'
      ci-files-changed:
        description: 'Boolean string result for if CI files have changed'
        type: string
        required: false
        default: 'false'
      doc-files-changed:
        description: 'Boolean string result for if documentation files have changed'
        type: string
        required: false
        default: 'false'
      src-files-changed:
        description: 'Boolean string result for if source files have changed'
        type: string
        required: false
        default: 'false'
      test-files-changed:
        description: 'Boolean string result for if test files have changed'
        type: string
        required: false
        default: 'false'
      gha-src-files-changed:
        description: 'Boolean string result for if GitHub Action source files have changed'
        type: string
        required: false
        default: 'false'
      gha-test-files-changed:
        description: 'Boolean string result for if GitHub Action test files have changed'
        type: string
        required: false
        default: 'false'
    outputs:
      new-release-detected:
        description: Boolean string result for if new release is available
        value: ${{ jobs.build.outputs.new-release-detected }}
      new-release-version:
        description: Version string for the new release
        value: ${{ jobs.build.outputs.new-release-version }}
      new-release-tag:
        description: Tag string for the new release
        value: ${{ jobs.build.outputs.new-release-tag }}
      new-release-is-prerelease:
        description: Boolean string result for if new release is a pre-release
        value: ${{ jobs.build.outputs.new-release-is-prerelease }}
      distribution-artifacts:
        description: Artifact Download name for the distribution artifacts
        value: ${{ jobs.build.outputs.distribution-artifacts }}
    # secrets: none required ATT


# set default Token permissions = none
permissions: {}


env:
  LOWEST_PYTHON_VERSION: '3.8'
  COMMON_PYTHON_VERSION: '3.11'


jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    if: ${{ inputs.build-files-changed == 'true' || inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' || inputs.gha-src-files-changed == 'true' || inputs.gha-test-files-changed == 'true' }}

    steps:
      - name: Setup | Checkout Repository at workflow sha
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          ref: ${{ github.sha }}
          fetch-depth: 0

      - name: Setup | Force correct release branch on workflow sha
        run: |
          git checkout -B ${{ github.ref_name }}

      - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }}
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
        with:
          python-version: ${{ env.COMMON_PYTHON_VERSION }}
          cache: 'pip'

      - name: Setup | Install dependencies
        run: |
          python -m pip install --upgrade pip setuptools wheel
          pip install -e .[build]

      - name: Build | Build next version artifacts
        id: version
        uses: python-semantic-release/python-semantic-release@02f2a5c74dbb6aa2989f10fc4af12cd8e6bf025f  # v10.5.2
        with:
          github_token: ""
          verbosity: 1
          build: true
          changelog: true
          commit: false
          push: false
          tag: false
          vcs_release: false

      - name: Build | Annotate next version
        if: steps.version.outputs.released == 'true'
        run: |
          printf '%s\n' "::notice::Next release will be '${{ steps.version.outputs.tag }}'"

      - name: Build | Create non-versioned distribution artifact
        if: steps.version.outputs.released == 'false'
        run: python -m build .

      - name: Build | Set distribution artifact variables
        id: build
        run: |
          printf '%s\n' "dist_dir=dist/*" >> $GITHUB_OUTPUT
          printf '%s\n' "artifacts_name=dist" >> $GITHUB_OUTPUT

      - name: Upload | Distribution Artifacts
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f  # v6.0.0
        with:
          name: ${{ steps.build.outputs.artifacts_name }}
          path: ${{ steps.build.outputs.dist_dir }}
          if-no-files-found: error
          retention-days: 2

    outputs:
      new-release-detected: ${{ steps.version.outputs.released }}
      new-release-version: ${{ steps.version.outputs.version }}
      new-release-tag: ${{ steps.version.outputs.tag }}
      new-release-is-prerelease: ${{ steps.version.outputs.is_prerelease }}
      distribution-artifacts: ${{ steps.build.outputs.artifacts_name }}


  unit-test:
    name: Unit Tests
    if: ${{ inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' }}
    runs-on: ubuntu-latest

    steps:
      - name: Setup | Checkout Repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          ref: ${{ github.sha }}
          fetch-depth: 1

      - name: Setup | Install Python ${{ env.LOWEST_PYTHON_VERSION }}
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
        with:
          python-version: ${{ env.LOWEST_PYTHON_VERSION }}
          cache: 'pip'

      - name: Setup | Install dependencies
        run: |
          python -m pip install --upgrade pip setuptools wheel
          pip install -e .[test]
          pip install pytest-github-actions-annotate-failures

      - name: Test | Run pytest -m unit --comprehensive
        id: tests
        env:
          COLUMNS: 150
        run: |
          pytest                        \
            -vv                         \
            -nauto                      \
            -m unit                     \
            --comprehensive             \
            --cov=semantic_release      \
            --cov-context=test          \
            --cov-report=term-missing   \
            --cov-fail-under=60         \
            --junit-xml=tests/reports/pytest-results.xml

      - name: Report | Upload Test Results
        uses: mikepenz/action-junit-report@e08919a3b1fb83a78393dfb775a9c37f17d8eea6  # v6.0.1
        if: ${{ always() && steps.tests.outcome != 'skipped' }}
        with:
          report_paths: ./tests/reports/*.xml
          annotate_only: true


  test-linux:
    name: Python ${{ matrix.python-version }} on ${{ matrix.os }} E2E tests
    runs-on: ${{ matrix.os }}
    needs:
      - build
      - unit-test
    if: ${{ inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' }}
    strategy:
      matrix:
        python-version: ${{ fromJson(inputs.python-versions-linux) }}
        os:
          - ubuntu-latest

    steps:
      - name: Setup | Checkout Repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          ref: ${{ github.sha }}
          fetch-depth: 1

      - name: Setup | Install Python ${{ matrix.python-version }}
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Setup | Download Distribution Artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131  # v7.0.0
        with:
          name: ${{ needs.build.outputs.distribution-artifacts }}
          path: ./dist

      - name: Setup | Install dependencies
        id: install
        # To ensure we are testing our installed package (not the src code), we must
        # uninstall the editable install (symlink) first then install the distribution artifact.
        # Lastly, we ask python to give us the installation location of our distribution artifact
        # so that we can use it in the pytest command for coverage
        run: |
          python -m pip install --upgrade pip setuptools wheel
          pip install -e .[test]
          pip install pytest-github-actions-annotate-failures
          pip uninstall -y python-semantic-release
          pip install dist/python_semantic_release-*.whl
          python -c 'import pathlib, semantic_release; print(f"PKG_INSTALLED_DIR={pathlib.Path(semantic_release.__file__).resolve().parent}")' >> $GITHUB_OUTPUT

      - name: Test | Run pytest -m e2e --comprehensive
        id: tests
        env:
          COLUMNS: 150
        run: |
          pytest                                                   \
            -vv                                                    \
            -nauto                                                 \
            -m e2e                                                 \
            --comprehensive                                        \
            --cov=${{ steps.install.outputs.PKG_INSTALLED_DIR }}   \
            --cov-context=test                                     \
            --cov-report=term-missing                              \
            --cov-fail-under=70                                    \
            --junit-xml=tests/reports/pytest-results.xml

      - name: Report | Upload Cached Repos on Failure
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f  # v6.0.0
        if: ${{ failure() && steps.tests.outcome == 'failure' }}
        with:
          name: ${{ format('cached-repos-{0}-{1}', matrix.os, matrix.python-version) }}
          path: .pytest_cache/d/psr-*
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

      - name: Report | Upload Tested Repos on Failure
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f  # v6.0.0
        if: ${{ failure() && steps.tests.outcome == 'failure' }}
        with:
          name: ${{ format('tested-repos-{0}-{1}', matrix.os, matrix.python-version) }}
          path: /tmp/pytest-of-runner/pytest-current/*
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

      - name: Report | Upload Test Results
        uses: mikepenz/action-junit-report@e08919a3b1fb83a78393dfb775a9c37f17d8eea6  # v6.0.1
        if: ${{ always() && steps.tests.outcome != 'skipped' }}
        with:
          report_paths: ./tests/reports/*.xml
          annotate_only: true


  test-windows:
    name: Python ${{ matrix.python-version }} on ${{ matrix.os }} E2E tests
    runs-on: ${{ matrix.os }}
    needs:
      - build
      - unit-test
    if: ${{ inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' }}
    strategy:
      matrix:
        python-version: ${{ fromJson(inputs.python-versions-windows) }}
        os: [windows-latest]

    steps:
      - name: Setup | Checkout Repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          ref: ${{ github.sha }}
          fetch-depth: 1

      - name: Setup | Install Python ${{ matrix.python-version }}
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Setup | Download Distribution Artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131  # v7.0.0
        with:
          name: ${{ needs.build.outputs.distribution-artifacts }}
          path: dist

      - name: Setup | Install dependencies
        id: install
        # To ensure we are testing our installed package (not the src code), we must
        # uninstall the editable install (symlink) first then install the distribution artifact.
        # Lastly, we ask python to give us the installation location of our distribution artifact
        # so that we can use it in the pytest command for coverage
        shell: pwsh
        run: |
          $ErrorActionPreference = 'stop'
          python -m pip install --upgrade pip setuptools wheel
          pip install -e .[test]
          pip install pytest-github-actions-annotate-failures
          pip uninstall -y python-semantic-release
          $psrWheelFile = Get-ChildItem dist\python_semantic_release-*.whl -File | Select-Object -Index 0
          pip install "$psrWheelFile"
          python -c 'import pathlib, semantic_release; print(f"PKG_INSTALLED_DIR={pathlib.Path(semantic_release.__file__).resolve().parent}")' | Tee-Object -Variable cmdOutput
          echo $cmdOutput >> $env:GITHUB_OUTPUT

      - name: Test | Run pytest -m e2e
        id: tests
        shell: pwsh

        # env:
          # Required for GitPython to work on Windows because of getpass.getuser()
          # USERNAME: "runneradmin"
          # COLUMNS: 150
          # Because GHA is currently broken on Windows to pass these varables, we do it manually
        run: |
          $env:USERNAME = "runneradmin"
          $env:COLUMNS = 150
          pytest                                                    `
            -vv                                                     `
            -nauto                                                  `
            -m e2e                                                  `
            `--cov=${{ steps.install.outputs.PKG_INSTALLED_DIR }}   `
            `--cov-context=test                                     `
            `--cov-report=term-missing                              `
            `--junit-xml=tests/reports/pytest-results.xml

      - name: Report | Upload Cached Repos on Failure
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f  # v6.0.0
        if: ${{ failure() && steps.tests.outcome == 'failure' }}
        with:
          name: ${{ format('cached-repos-{0}-{1}', matrix.os, matrix.python-version) }}
          path: .pytest_cache/d/psr-*
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

      - name: Report | Upload Tested Repos on Failure
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f  # v6.0.0
        if: ${{ failure() && steps.tests.outcome == 'failure' }}
        with:
          name: ${{ format('tested-repos-{0}-{1}', matrix.os, matrix.python-version) }}
          path: ~/AppData/Local/Temp/pytest-of-runneradmin/pytest-current/*
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

      - name: Report | Upload Test Results
        uses: mikepenz/action-junit-report@e08919a3b1fb83a78393dfb775a9c37f17d8eea6  # v6.0.1
        if: ${{ always() && steps.tests.outcome != 'skipped' }}
        with:
          report_paths: ./tests/reports/*.xml
          annotate_only: true


  test-gh-action:
    name: Validate Action Build & Execution
    runs-on: ubuntu-latest
    if: ${{ inputs.gha-src-files-changed == 'true' || inputs.gha-test-files-changed == 'true' || inputs.ci-files-changed == 'true' }}

    needs:
      - build

    env:
      TEST_CONTAINER_TAG: psr-action:latest
      ACTION_SRC_DIR: src/gh_action

    steps:
      - name: Setup | Checkout Repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          fetch-depth: 1
          ref: ${{ github.sha }}

      - name: Setup | Download Distribution Artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131  # v7.0.0
        with:
          name: ${{ needs.build.outputs.distribution-artifacts }}
          path: ${{ env.ACTION_SRC_DIR }}

      - name: Setup | Update Dependency list with latest version
        working-directory: ${{ env.ACTION_SRC_DIR }}
        run: |
          find . -name '*.whl' > requirements.txt

      - name: Setup | Allow Docker build to include wheel files
        working-directory: ${{ env.ACTION_SRC_DIR }}
        run: |
          printf '%s\n' "!*.whl" >> .dockerignore

      - name: Build | Action Container
        id: container-builder
        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83  # v6.18.0
        with:
          context: ${{ env.ACTION_SRC_DIR }}
          load: true # add to `docker images`
          push: false
          platforms: linux/amd64
          tags: ${{ env.TEST_CONTAINER_TAG }}

      - name: Test | Action Container
        run: bash tests/gh_action/run.sh


  lint:
    name: Lint
    if: ${{ inputs.files-changed == 'true' }}
    runs-on: ubuntu-latest

    steps:
      - name: Setup | Checkout Repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8  # v6.0.1
        with:
          ref: ${{ github.sha }}
          fetch-depth: 1

      - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }}
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548  # v6.1.0
        with:
          python-version: ${{ env.COMMON_PYTHON_VERSION }}
          cache: 'pip'

      - name: Setup | Install dependencies
        run: |
          python -m pip install --upgrade pip setuptools wheel
          pip install -e .[dev,mypy,test]
        # needs test because we run mypy over the tests as well and without the dependencies
        # mypy will throw import errors

      - name: Lint | Ruff Evaluation
        id: lint
        run: |
          ruff check \
            --config pyproject.toml \
            --output-format=full \
            --exit-non-zero-on-fix

      - name: Type-Check | MyPy Evaluation
        id: type-check
        if: ${{ always() && steps.lint.outcome != 'skipped' }}
        run: |
          mypy .

      - name: Format-Check | Ruff Evaluation
        id: format-check
        if: ${{ always() && steps.type-check.outcome != 'skipped' }}
        run: |
          ruff format --check --config pyproject.toml
