name: Pre-Release Pipeline

on:
  pull_request:
    types: [closed]
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  prerelease-tag:
    name: Tag and Create Pre-Release
    if: >
      github.event.pull_request.merged == true
      && github.event.pull_request.head.ref == 'prerelease/next'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    outputs:
      tag_name: ${{ steps.tag.outputs.tag_name }}
      version: ${{ steps.tag.outputs.version }}
    steps:
      - name: Generate GitHub App Token
        id: app-token
        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v2.0.0
        with:
          app-id: ${{ vars.RELEASE_APP_ID }}
          private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

      - name: Checkout code
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          persist-credentials: false

      - name: Extract version and create tag
        id: tag
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          REPO: ${{ github.repository }}
          MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
        run: |
          VERSION=$(echo "$PR_TITLE" | grep -oE 'pre-release [0-9]+\.[0-9]+\.[0-9]+' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
          if [ -z "$VERSION" ]; then
            echo "⚠️ Could not extract version from PR title, falling back to manifest"
            VERSION=$(jq -r '.["."] // "0.0.0"' .release-please-manifest.json 2>/dev/null || echo "")
          fi
          if [ -z "$VERSION" ]; then
            echo "::error::Could not extract version from PR title or manifest: $PR_TITLE"
            exit 1
          fi

          # Even/odd convention: pre-release versions must have odd minor
          PRE_MINOR=$(echo "$VERSION" | cut -d. -f2)
          if (( PRE_MINOR % 2 == 0 )); then
            echo "::error::Pre-release version $VERSION must have an odd minor (even/odd convention)"
            exit 1
          fi

          TAG="hve-core-v${VERSION}"

          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "tag_name=$TAG" >> "$GITHUB_OUTPUT"

          if ! gh api "/repos/$REPO/git/refs/tags/$TAG" --silent 2>/dev/null; then
            gh api "/repos/$REPO/git/refs" \
              -f "ref=refs/tags/$TAG" \
              -f "sha=$MERGE_SHA"
            echo "Created tag $TAG"
          else
            echo "Tag $TAG already exists"
          fi

      - name: Create draft pre-release GitHub Release
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          TAG: ${{ steps.tag.outputs.tag_name }}
          VERSION: ${{ steps.tag.outputs.version }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
        run: |
          BODY=$(gh pr view "$PR_NUMBER" \
            --json body --jq '.body' \
            -R "$REPO")

          gh release create "$TAG" \
            --title "hve-core $VERSION (pre-release)" \
            --prerelease \
            --draft \
            --notes "$BODY" \
            -R "$REPO"

  close-milestone:
    name: Close Pre-Release Milestone
    needs: [prerelease-tag]
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Close milestone for pre-release version
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ needs.prerelease-tag.outputs.version }}
        run: |
          REPO="${{ github.repository }}"
          MAJOR=$(echo "$VERSION" | cut -d. -f1)
          MINOR=$(echo "$VERSION" | cut -d. -f2)
          TITLE="v${MAJOR}.${MINOR}.0"
          NUMBER=$(gh api "/repos/$REPO/milestones?state=open&per_page=100" \
            --jq ".[] | select(.title == \"$TITLE\") | .number")
          if [ -n "$NUMBER" ]; then
            gh api "/repos/$REPO/milestones/$NUMBER" \
              -X PATCH -f state=closed
            echo "✅ Closed milestone $TITLE (#$NUMBER)"
          else
            echo "ℹ️ No open milestone found matching '$TITLE' — skipping"
          fi

  extension-package-prerelease:
    name: Package VS Code Extensions (Pre-Release)
    needs: [prerelease-tag]
    uses: ./.github/workflows/extension-package.yml
    with:
      version: ${{ needs.prerelease-tag.outputs.version }}
      channel: PreRelease
    permissions:
      contents: read

  generate-dependency-sbom:
    name: Generate Dependency SBOM
    needs: [prerelease-tag]
    runs-on: ubuntu-latest
    permissions:
      # write required for gh release upload to attach SBOM to the release
      contents: write
    steps:
      - name: Checkout dependency manifests
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          sparse-checkout: |
            package.json
            package-lock.json
            .syft.yaml
          sparse-checkout-cone-mode: false
          persist-credentials: false

      - name: Generate dependency SBOM
        uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
        with:
          path: .
          format: spdx-json
          output-file: dependencies.spdx.json
          upload-release-assets: false
          upload-artifact: false
          config: .syft.yaml
          dependency-snapshot: true

      - name: Upload dependency SBOM artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: sbom-dependencies
          path: dependencies.spdx.json

      - name: Upload dependency SBOM to release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release upload "${{ needs.prerelease-tag.outputs.tag_name }}" \
            "dependencies.spdx.json" \
            --clobber -R "${{ github.repository }}"

  attest-and-upload:
    name: Attest and Upload (${{ matrix.id }})
    needs: [prerelease-tag, extension-package-prerelease, generate-dependency-sbom]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.extension-package-prerelease.outputs.collections-matrix) }}
    permissions:
      contents: write
      id-token: write
      attestations: write
      artifact-metadata: write
    steps:
      - name: Checkout Syft config
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          sparse-checkout: .syft.yaml
          sparse-checkout-cone-mode: false
          persist-credentials: false

      - name: Download dependency SBOM
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: sbom-dependencies
          path: ./sbom

      - name: Download VSIX artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: extension-vsix-${{ matrix.id }}
          path: ./dist

      - name: Resolve VSIX filename
        id: vsix
        run: |
          shopt -s nullglob
          files=(dist/*.vsix)
          if [[ ${#files[@]} -eq 0 ]]; then
            echo "::error::No VSIX file found in dist/"
            exit 1
          fi
          if [[ ${#files[@]} -gt 1 ]]; then
            echo "::error::Expected exactly one VSIX file but found ${#files[@]}: ${files[*]}"
            exit 1
          fi
          VSIX_FILE="${files[0]}"
          VSIX_NAME=$(basename "$VSIX_FILE")
          echo "file=$VSIX_FILE" >> "$GITHUB_OUTPUT"
          echo "name=$VSIX_NAME" >> "$GITHUB_OUTPUT"

      - name: Generate SBOM
        uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
        with:
          file: ${{ steps.vsix.outputs.file }}
          format: spdx-json
          output-file: dist/${{ steps.vsix.outputs.name }}.spdx.json
          artifact-name: sbom-${{ matrix.id }}
          upload-release-assets: false
          upload-artifact: true
          config: .syft.yaml

      - name: Verify SBOM files exist
        run: |
          test -f "dist/${{ steps.vsix.outputs.name }}.spdx.json" \
            || { echo "::error::Per-VSIX SBOM not found: dist/${{ steps.vsix.outputs.name }}.spdx.json"; exit 1; }
          test -f "sbom/dependencies.spdx.json" \
            || { echo "::error::Dependency SBOM not found: sbom/dependencies.spdx.json"; exit 1; }

      - name: Attest build provenance
        id: attest
        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
        with:
          subject-path: ${{ steps.vsix.outputs.file }}

      - name: Attest SBOM
        uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
        with:
          subject-path: ${{ steps.vsix.outputs.file }}
          sbom-path: dist/${{ steps.vsix.outputs.name }}.spdx.json

      - name: Attest dependency SBOM
        uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
        with:
          subject-path: ${{ steps.vsix.outputs.file }}
          sbom-path: sbom/dependencies.spdx.json

      - name: Upload assets to GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
          BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }}
        run: |
          cp "$BUNDLE_PATH" \
            "dist/${{ steps.vsix.outputs.name }}.sigstore.json"
          jq -c '.dsseEnvelope' "$BUNDLE_PATH" > \
            "dist/${{ steps.vsix.outputs.name }}.intoto.jsonl"
          gh release upload "${{ needs.prerelease-tag.outputs.tag_name }}" \
            "${{ steps.vsix.outputs.file }}" \
            "dist/${{ steps.vsix.outputs.name }}.spdx.json" \
            "dist/${{ steps.vsix.outputs.name }}.sigstore.json" \
            "dist/${{ steps.vsix.outputs.name }}.intoto.jsonl" \
            --clobber -R "${{ github.repository }}"

  publish-release:
    name: Publish Pre-Release
    needs: [prerelease-tag, attest-and-upload]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Publish GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.prerelease-tag.outputs.tag_name }}
          REPO: ${{ github.repository }}
        run: |
          gh release edit "$TAG" --draft=false -R "$REPO"
