name: Release

on:
  push:
    tags:
      - "v*" # Match tags like v1.0.0, v2.1.3 etc.

permissions:
  contents: read # checkout repository

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

env:
  CARGO_TERM_COLOR: always

jobs:
  # 1) Build & push per-arch images, sign each by digest
  build-oci-images:
    name: Build OCI image (${{ matrix.arch }})
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-24.04
            arch: amd64
            platform: linux/amd64
          - os: ubuntu-24.04-arm
            arch: arm64
            platform: linux/arm64
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read # checkout repository
      packages: write # push container images to GHCR
      id-token: write # keyless cosign signing

    outputs:
      digest_amd64: ${{ steps.meta.outputs.digest_amd64 }}
      digest_arm64: ${{ steps.meta.outputs.digest_arm64 }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          submodules: true
          persist-credentials: false

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build & push (by platform) with BuildKit cache
        id: build
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: .
          push: true
          platforms: ${{ matrix.platform }}
          tags: |
            ghcr.io/${{ github.repository_owner }}/hyper-mcp-remote:${{ github.ref_name }}-${{ matrix.arch }}
          cache-from: type=gha,scope=hyper-mcp-remote-release-${{ matrix.arch }}
          cache-to: type=gha,mode=max,scope=hyper-mcp-remote-release-${{ matrix.arch }}

      - name: Resolve and sign arch image by digest
        id: meta
        shell: bash
        run: |
          set -euo pipefail

          IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote"
          DIGEST="${STEPS_BUILD_OUTPUTS_DIGEST}"

          REF="${IMAGE}@${DIGEST}"
          echo "Signing arch image by digest: ${REF}"
          cosign sign --yes "${REF}"

          if [[ "${ARCH}" == "amd64" ]]; then
            echo "digest_amd64=${DIGEST}" >> "$GITHUB_OUTPUT"
          elif [[ "${ARCH}" == "arm64" ]]; then
            echo "digest_arm64=${DIGEST}" >> "$GITHUB_OUTPUT"
          else
            echo "Unknown arch: ${ARCH}" >&2
            exit 1
          fi

          echo "Built ${IMAGE}:${REF_NAME}-${ARCH} -> ${DIGEST}"
        env:
          REPOSITORY_OWNER: ${{ github.repository_owner }}
          ARCH: ${{ matrix.arch }}
          STEPS_BUILD_OUTPUTS_DIGEST: ${{ steps.build.outputs.digest }}
          REF_NAME: ${{ github.ref_name }}

  # 2) Create multi-arch manifests, sign manifests by digest, export digests for the GH release body
  create-multiarch-manifests:
    name: Create & sign multi-arch manifests
    needs: build-oci-images
    runs-on: ubuntu-latest
    permissions:
      contents: read # checkout repository
      packages: write # push manifest to GHCR
      id-token: write # keyless cosign signing

    outputs:
      tag_digest: ${{ steps.digest.outputs.tag_digest }}
      latest_digest: ${{ steps.digest.outputs.latest_digest }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Create, push, and sign manifests by digest
        id: digest
        shell: bash
        run: |
          set -euo pipefail

          TAG="${GITHUB_REF#refs/tags/}"
          IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote"

          AMD64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64}"
          ARM64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64}"

          if [[ -z "${AMD64_DIGEST}" || -z "${ARM64_DIGEST}" ]]; then
            echo "Missing per-arch digests from build job." >&2
            exit 1
          fi

          # ---------------- Tag manifest ----------------
          echo "Creating multi-arch manifest: ${IMAGE}:${TAG}"
          docker buildx imagetools create \
            -t "${IMAGE}:${TAG}" \
            "${IMAGE}@${AMD64_DIGEST}" \
            "${IMAGE}@${ARM64_DIGEST}"

          TAG_DIGEST="$(docker buildx imagetools inspect "${IMAGE}:${TAG}" | grep -E '^Digest:' | awk '{print $2}')"
          echo "tag_digest=${TAG_DIGEST}" >> "$GITHUB_OUTPUT"

          echo "Signing tag manifest by digest: ${IMAGE}@${TAG_DIGEST}"
          cosign sign --yes "${IMAGE}@${TAG_DIGEST}"

          # ---------------- Latest manifest ----------------
          echo "Creating multi-arch manifest: ${IMAGE}:latest"
          docker buildx imagetools create \
            -t "${IMAGE}:latest" \
            "${IMAGE}@${AMD64_DIGEST}" \
            "${IMAGE}@${ARM64_DIGEST}"

          LATEST_DIGEST="$(
            docker buildx imagetools inspect "${IMAGE}:latest" --format '{{json .Manifest}}' \
              | python3 -c 'import json,sys; print(json.load(sys.stdin)["digest"])'
          )"

          if [[ ! "$LATEST_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
            echo "ERROR: Invalid digest: '$LATEST_DIGEST'" >&2
            exit 1
          fi

          echo "latest_digest=${LATEST_DIGEST}" >> "$GITHUB_OUTPUT"

          echo "Signing latest manifest by digest: ${IMAGE}@${LATEST_DIGEST}"
          cosign sign --yes "${IMAGE}@${LATEST_DIGEST}"

          echo "Resolved ${IMAGE}:${TAG}    -> ${TAG_DIGEST}"
          echo "Resolved ${IMAGE}:latest  -> ${LATEST_DIGEST}"
        env:
          REPOSITORY_OWNER: ${{ github.repository_owner }}
          NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64: ${{ needs.build-oci-images.outputs.digest_amd64 }}
          NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64: ${{ needs.build-oci-images.outputs.digest_arm64 }}

  # 3) Build release binaries (matrix), upload as artifacts (no GH release here)
  build-release-binaries:
    name: Build release binaries (${{ matrix.target }})
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            ext: ""
          - os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            ext: ""
          - os: macos-latest
            target: aarch64-apple-darwin
            ext: ""
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            ext: ".exe"

    runs-on: ${{ matrix.os }}
    permissions:
      contents: read # checkout repository

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Install Rust toolchain
        run: rustup show

      - name: Install cargo-auditable
        run: cargo install cargo-auditable

      - name: Install target
        shell: bash
        run: rustup target add "${TARGET}"
        env:
          TARGET: ${{ matrix.target }}

      - name: Build
        shell: bash
        run: cargo auditable build --target "${TARGET}" --release --locked
        env:
          TARGET: ${{ matrix.target }}

      - name: Package + checksums
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p dist

          bin="hyper-mcp-remote${EXT}"
          src="target/${TARGET}/release/${bin}"

          if [[ "${RUNNER_OS}" == "Windows" ]]; then
            # Use PowerShell's Compress-Archive? We'll keep it simple with tar if available;
            # but Windows runners have bsdtar via Git. Use zip for compatibility.
            7z a "dist/hyper-mcp-remote-${TARGET}.zip" "${src}"
            pkg="dist/hyper-mcp-remote-${TARGET}.zip"
          else
            tar -czf "dist/hyper-mcp-remote-${TARGET}.tar.gz" -C "target/${TARGET}/release" "${bin}"
            pkg="dist/hyper-mcp-remote-${TARGET}.tar.gz"
          fi

          # checksums
          if command -v sha256sum >/dev/null 2>&1; then
            sha256sum "${pkg}" > "dist/checksums-${TARGET}.txt"
          elif command -v shasum >/dev/null 2>&1; then
            shasum -a 256 "${pkg}" > "dist/checksums-${TARGET}.txt"
          elif [[ "$RUNNER_OS" == "Windows" ]]; then
            powershell.exe -NoProfile -Command \
              "\$h=(Get-FileHash -Algorithm SHA256 '${pkg}').Hash.ToLower(); \"\$h  ${pkg}\" | Out-File -Encoding ascii 'dist/checksums-${TARGET}.txt'"
          else
            echo "No SHA256 tool found" >&2
            exit 1
          fi
        env:
          TARGET: ${{ matrix.target }}
          EXT: ${{ matrix.ext }}
          RUNNER_OS: ${{ runner.os }}

      - name: Upload artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: release-${{ matrix.target }}
          path: |
            dist/hyper-mcp-remote-${{ matrix.target }}.tar.gz
            dist/hyper-mcp-remote-${{ matrix.target }}.zip
            dist/checksums-${{ matrix.target }}.txt
          if-no-files-found: error

  sbom:
    name: SBOM
    runs-on: ubuntu-latest
    permissions:
      contents: read # checkout repository

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          submodules: true
          persist-credentials: false

      - name: Install cargo-cyclonedx
        run: cargo install cargo-cyclonedx --locked

      - name: Generate CycloneDX SBOM
        run: cargo cyclonedx -f json --all-features --override-filename temp

      - name: Clean SBOM
        run: jq '
          del(.metadata.component."bom-ref")
          | del(.metadata.component.purl)
          | del(.metadata.component.components[0]."bom-ref")
          | del(.metadata.component.components[0].purl)
          ' temp.json > sbom.cdx.json

      - name: Upload sbom artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: sbom-cdx-json
          path: sbom.cdx.json
          if-no-files-found: error

  # 4) Publish GitHub Release once (download artifacts, write body incl digest, upload assets)
  publish-github-release:
    name: Publish GitHub Release
    needs:
      - build-release-binaries
      - create-multiarch-manifests
      - sbom
    runs-on: ubuntu-latest
    permissions:
      contents: write # create GitHub release and upload assets
    env:
      GH_TOKEN: ${{ github.token }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Download build artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: release-*
          path: dist

      - name: Flatten artifacts
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p out
          find dist -type f -maxdepth 3 -print -exec cp {} out/ \;

      - name: Download sbom artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: sbom-cdx-json
          path: .

      - name: Write release body (include immutable digest)
        shell: bash
        run: |
          set -euo pipefail

          TAG="${GITHUB_REF#refs/tags/}"
          IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote"
          TAG_DIGEST="${NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_TAG_DIGEST}"
          SHA="${GITHUB_SHA}"

          cat > release-body.md <<EOF
          Final release for \`${TAG}\`.

          Commit:
          - \`${SHA}\`

          Included:
          - hyper-mcp-remote binaries for Linux & macOS
          - hyper-mcp-remote container image: \`${IMAGE}:${TAG}\`

          ✅ Multi-arch digest (immutable, recommended for pinning):
          - \`${IMAGE}@${TAG_DIGEST}\`

          All container images are signed with Cosign. Verify with:

          \`\`\`bash
          cosign verify \
            --certificate-identity "https://github.com/hyper-mcp-rs/hyper-mcp-remote/.github/workflows/release.yml@refs/tags/${TAG}" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            ${IMAGE}@${TAG_DIGEST}
          \`\`\`
          EOF
        env:
          REPOSITORY_OWNER: ${{ github.repository_owner }}
          NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_TAG_DIGEST: ${{ needs.create-multiarch-manifests.outputs.tag_digest }}

      - name: Create GitHub Release
        shell: bash
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          gh release create "${TAG}" \
            --title "Release ${TAG}" \
            --generate-notes \
            --notes-file release-body.md \
            out/* \
            sbom.cdx.json
