name: EEP Release — Publish npm + PyPI

# Triggered by version tags. Publish jobs are gated behind the `release`
# environment, which requires manual reviewer approval before secrets or
# OIDC tokens are issued. See RELEASING.md for the full release runbook.

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'       # e.g. v0.1.0, v1.0.0
      - 'v[0-9]+.[0-9]+.[0-9]+-*'     # pre-releases: v0.1.0-alpha.1

permissions:
  contents: read

jobs:
  # ── Verify all tests pass before any publish step ─────────────────────────
  preflight:
    name: Preflight — all tests must pass
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v6 # renovate: pin

      # TS packages
      - uses: actions/setup-node@v6 # renovate: pin
        with:
          node-version: '22'

      - name: Test schema suite
        run: npm ci && npx vitest run
        working-directory: tests

      - name: Test @eep-dev/gates
        run: npm ci && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/gates

      - name: Test @eep-dev/signer
        run: npm ci && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/signer

      - name: Test @eep-dev/validator
        run: npm ci && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/validator

      - name: Test @eep-dev/compliance-cli
        run: npm ci && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/compliance-cli

      - name: Test @eep-dev/discovery
        run: npm install && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/discovery

      - name: Test @eep-dev/mcp-bridge
        run: |
          (cd ../gates && npm ci && npm run build)
          npm install && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/mcp-bridge

      - name: Test @eep-dev/middleware
        run: |
          for dep in gates signer validator; do
            (cd "../$dep" && npm ci && npm run build)
          done
          npm install && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/middleware

      - name: Test @eep-dev/setup-cli
        # build produces dist/; agent-adopt's tsc resolves @eep-dev/setup-cli types from dist/
        run: npm install && npm run build && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/setup-cli

      - name: Test @eep-dev/agent-adopt
        run: npm install && npm run build && node dist/index.js --help && npx vitest run --reporter=dot
        working-directory: packages/@eep-dev/agent-adopt

      - name: npm high-severity audit gate
        run: |
          set -euo pipefail
          for dir in \
            "packages/@eep-dev/gates" \
            "packages/@eep-dev/signer" \
            "packages/@eep-dev/validator" \
            "packages/@eep-dev/compliance-cli" \
            "packages/@eep-dev/setup-cli" \
            "packages/@eep-dev/agent-adopt" \
            "packages/@eep-dev/middleware" \
            "tests" \
            "examples/node-gate-publisher"
          do
            echo "Auditing $dir"
            (cd "$dir" && npm ci && npm audit --omit=dev --audit-level=high)
          done
          echo "Auditing packages/@eep-dev/discovery"
          (cd "packages/@eep-dev/discovery" && npm install && npm audit --omit=dev --audit-level=high)
          echo "Auditing packages/@eep-dev/mcp-bridge"
          (cd "packages/@eep-dev/mcp-bridge" && npm install && npm audit --omit=dev --audit-level=high)

      # Python packages
      - uses: actions/setup-python@v6 # renovate: pin
        with:
          python-version: '3.12'

      - name: Test eep-gates-python
        run: pip install -e '.[dev]' && PYTHONPATH=. python -m pytest tests/ -v
        working-directory: packages/eep-gates-python

      - name: Test eep-signer-python
        run: pip install pytest && PYTHONPATH=. python -m pytest tests/ -v
        working-directory: packages/eep-signer-python

      - name: Test eep-validator-python
        run: pip install pytest pytest-asyncio && PYTHONPATH=. python -m pytest tests/ -v
        working-directory: packages/eep-validator-python

      - name: Test eep-compliance-cli-python
        run: pip install httpx pytest && PYTHONPATH=. python -m pytest tests/ -v
        working-directory: packages/eep-compliance-cli-python

      - name: Test eep-discovery-python
        run: pip install pytest && PYTHONPATH=. python -m pytest tests/ -v
        working-directory: packages/eep-discovery-python

      - name: Test eep-mcp-bridge-python
        run: pip install -e '.[dev]' && PYTHONPATH=. python -m pytest tests/ -q --no-header --no-summary
        working-directory: packages/eep-mcp-bridge-python

      - name: Test eep-middleware-python
        run: pip install -e '.[dev]' && python -m pytest -q
        working-directory: packages/eep-middleware-python

      # @eep-dev/gates ships TypeScript only; dist/ is gitignored. The example resolves
      # file:../../packages/@eep-dev/gates → dist/index.js must exist before start.
      - name: Build @eep-dev/gates
        working-directory: packages/@eep-dev/gates
        run: |
          npm ci
          npm run build

      - name: pip vulnerability audit gate
        run: |
          python -m venv .audit-venv
          . .audit-venv/bin/activate
          pip install --upgrade pip pip-audit
          pip-audit -r tests/cross-impl/requirements.txt
          pip install \
            ./packages/eep-gates-python \
            ./packages/eep-signer-python \
            ./packages/eep-validator-python \
            ./packages/eep-compliance-cli-python \
            ./packages/eep-discovery-python \
            ./packages/eep-mcp-bridge-python \
            ./packages/eep-middleware-python
          pip-audit

      - name: Start node gate publisher for cross-impl tests
        run: |
          npm install
          npx tsx server.ts > /tmp/eep-gate-publisher.log 2>&1 &
          echo "PUBLISHER_PID=$!" >> $GITHUB_ENV
        working-directory: examples/node-gate-publisher

      - name: Wait for publisher readiness
        run: |
          for i in {1..30}; do
            if curl -fsS "http://localhost:3002/.well-known/eep.json" > /dev/null; then
              echo "Publisher is ready"
              exit 0
            fi
            sleep 1
          done
          echo "Publisher failed to start"
          cat /tmp/eep-gate-publisher.log || true
          exit 1

      - name: Run cross-implementation protocol tests
        run: |
          pip install -r tests/cross-impl/requirements.txt
          EEP_BASE_URL=http://localhost:3002 python -m pytest tests/cross-impl -v

      - name: Stop publisher
        if: always()
        run: |
          if [ -n "${PUBLISHER_PID:-}" ]; then
            kill "$PUBLISHER_PID" || true
          fi

  # ── SBOM generation ───────────────────────────────────────────────────────
  # Emits a CycloneDX SBOM for the repo. The workflow artifact is attached to the
  # GitHub Release in create-github-release (not here — avoids contents:write on this job).
  sbom:
    name: Generate SBOM
    runs-on: ubuntu-latest
    needs: preflight
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v6 # renovate: pin

      - name: Generate CycloneDX SBOM (repository scope)
        uses: anchore/sbom-action@v0 # renovate: pin
        with:
          path: .
          format: cyclonedx-json
          output-file: eep-sbom.cdx.json
          artifact-name: eep-sbom.cdx.json
          # SBOM ships on the Release via create-github-release (artifact download).
          # Default true tries to attach here with contents:read and fails on public repos.
          upload-release-assets: false

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v7 # renovate: pin
        with:
          name: eep-sbom
          path: eep-sbom.cdx.json
          retention-days: 90

  # ── npm publish (TypeScript packages) ─────────────────────────────────────
  # Requires manual approval via the `release` environment. Publishes with
  # SLSA build provenance attestations (npm publish --provenance).
  publish-npm:
    name: Publish npm packages
    runs-on: ubuntu-latest
    needs:
      - preflight
      - sbom
    environment:
      name: release
      url: https://www.npmjs.com/org/eep-dev
    permissions:
      contents: read
      id-token: write   # required for npm provenance via OIDC
    steps:
      - uses: actions/checkout@v6 # renovate: pin

      - uses: actions/setup-node@v6 # renovate: pin
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'

      - name: Extract version from tag
        id: version
        run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

      - name: Publish @eep-dev/signer
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/signer "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/validator
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/validator "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/gates
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/gates "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/discovery
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/discovery "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/middleware
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/middleware "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
          PRE_PUBLISH: >-
            cd ../gates && npm ci && npm run build &&
            node ../../../scripts/npm-publish-rewrite-deps.mjs . ${{ steps.version.outputs.VERSION }}

      - name: Publish @eep-dev/mcp-bridge
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/mcp-bridge "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
          PRE_PUBLISH: >-
            cd ../gates && npm ci && npm run build &&
            node ../../../scripts/npm-publish-rewrite-deps.mjs . ${{ steps.version.outputs.VERSION }}

      - name: Publish @eep-dev/compliance-cli
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/compliance-cli "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/setup-cli
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/setup-cli "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Publish @eep-dev/agent-adopt
        run: bash scripts/ci-npm-publish-package.sh packages/@eep-dev/agent-adopt "${{ steps.version.outputs.VERSION }}"
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
          PRE_PUBLISH: >-
            cd ../setup-cli && npm install && npm run build &&
            node ../../../scripts/npm-publish-rewrite-deps.mjs . ${{ steps.version.outputs.VERSION }}

  # ── PyPI publish (Python packages via OIDC trusted publishing) ───────────
  # Each PyPI project must have a Trusted Publisher configured pointing at
  # this repo, this workflow, and the `release` environment. No PyPI token
  # is used or required.
  publish-pypi:
    name: Publish PyPI packages
    runs-on: ubuntu-latest
    needs:
      - preflight
      - sbom
    environment:
      name: release
      url: https://pypi.org/project/eep-gates/
    permissions:
      contents: read
      id-token: write   # required for PyPI Trusted Publishing (OIDC)
    strategy:
      fail-fast: false
      max-parallel: 1   # one upload at a time — avoids PyPI upload API 429
      matrix:
        package:
          - eep-signer-python
          - eep-validator-python
          - eep-gates-python
          - eep-discovery-python
          - eep-compliance-cli-python
          - eep-mcp-bridge-python
          - eep-middleware-python
    steps:
      - uses: actions/checkout@v6 # renovate: pin

      - uses: actions/setup-python@v6 # renovate: pin
        with:
          python-version: '3.12'

      - name: Install build tools
        run: pip install --upgrade pip build

      - name: Extract version from tag
        id: version
        run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

      - name: Check if version already on PyPI
        id: pypi_release
        working-directory: packages/${{ matrix.package }}
        env:
          VERSION: ${{ steps.version.outputs.VERSION }}
        run: |
          set -euo pipefail
          PROJECT="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])")"
          echo "project=${PROJECT}" >> "$GITHUB_OUTPUT"
          HTTP_CODE="$(curl -sS -o /tmp/pypi-project.json -w '%{http_code}' "https://pypi.org/pypi/${PROJECT}/json")"
          if [ "${HTTP_CODE}" = "200" ] && VERSION="${VERSION}" python - <<'PY'
          import json, os, sys
          version = os.environ["VERSION"]
          data = json.load(open("/tmp/pypi-project.json"))
          if version in data.get("releases", {}):
              sys.exit(0)
          sys.exit(1)
          PY
          then
            echo "skip=true" >> "$GITHUB_OUTPUT"
            echo "PyPI already has ${PROJECT}@${VERSION} — skipping build and upload."
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
            echo "Will publish ${PROJECT}@${VERSION} to PyPI."
          fi

      - name: Already on PyPI (skipped)
        if: steps.pypi_release.outputs.skip == 'true'
        run: |
          echo "No upload needed for ${{ steps.pypi_release.outputs.project }}@${{ steps.version.outputs.VERSION }}."

      - name: Set version and build
        if: steps.pypi_release.outputs.skip != 'true'
        working-directory: packages/${{ matrix.package }}
        env:
          VERSION: ${{ steps.version.outputs.VERSION }}
        run: |
          # Update version in pyproject.toml from tag (idempotent, allows same).
          python - <<'PY'
          import os, pathlib, re
          version = os.environ['VERSION']
          path = pathlib.Path('pyproject.toml')
          text = path.read_text()
          new = re.sub(r'^version\s*=\s*".*?"', f'version = "{version}"', text, count=1, flags=re.M)
          path.write_text(new)
          PY
          python -m build

      - name: Publish to PyPI (Trusted Publishing)
        if: steps.pypi_release.outputs.skip != 'true'
        uses: pypa/gh-action-pypi-publish@release/v1 # renovate: pin
        with:
          packages-dir: packages/${{ matrix.package }}/dist/

  # ── GitHub Release creation with cosign keyless signatures ────────────────
  create-github-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    needs:
      - publish-npm
      - publish-pypi
    environment:
      name: release
    permissions:
      contents: write    # required for Release creation
      id-token: write    # required for sigstore/cosign keyless signing
    steps:
      - uses: actions/checkout@v6 # renovate: pin
        with:
          fetch-depth: 0

      - name: Download SBOM artifact
        uses: actions/download-artifact@v8 # renovate: pin
        with:
          name: eep-sbom
          path: ./release-artifacts

      - name: Build whitepaper PDF
        # Best-effort. Whitepaper is informational; failure should not block
        # the release. We still ship release_notes.txt and SBOM.
        continue-on-error: true
        run: |
          sudo apt-get update -y
          sudo apt-get install -y --no-install-recommends texlive-latex-base \
            texlive-latex-recommended texlive-fonts-recommended texlive-latex-extra \
            latexmk
          (cd docs && latexmk -pdf -interaction=nonstopmode WHITEPAPER.tex)
          cp docs/WHITEPAPER.pdf ./release-artifacts/eep-whitepaper.pdf || true

      - name: Build conformance fixtures tarball
        run: |
          if [ -d "tests/conformance-fixtures" ]; then
            tar -czf ./release-artifacts/eep-conformance-vectors-${GITHUB_REF_NAME}.tar.gz \
              -C tests conformance-fixtures
          fi

      - name: Extract version and changelog
        id: meta
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
          # Extract the section for this version from CHANGELOG.md
          awk "/^## \[$VERSION\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md > release_notes.txt
          if [ ! -s release_notes.txt ]; then
            {
              echo "Release v$VERSION"
              echo
              echo "No exact changelog section matched this tag. See CHANGELOG.md for details."
            } > release_notes.txt
          fi
          cp release_notes.txt ./release-artifacts/

      - name: Install cosign
        uses: sigstore/cosign-installer@v3 # renovate: pin

      - name: Sign release artifacts (sigstore keyless)
        run: |
          set -euo pipefail
          cd ./release-artifacts
          for f in *; do
            if [ -f "$f" ] && [[ "$f" != *.sig && "$f" != *.pem ]]; then
              echo "Signing $f"
              cosign sign-blob --yes \
                --output-signature "${f}.sig" \
                --output-certificate "${f}.pem" \
                "$f"
            fi
          done

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v3 # renovate: pin
        with:
          name: "EEP v${{ steps.meta.outputs.VERSION }}"
          body_path: release-artifacts/release_notes.txt
          draft: false
          prerelease: ${{ contains(github.ref_name, '-') }}
          generate_release_notes: false
          files: |
            release-artifacts/*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
