name: Nightly Release

on:
  schedule:
    - cron: "0 0 * * *" # midnight GMT
  workflow_dispatch:

permissions:
  contents: read # checkout repository

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

env:
  CARGO_TERM_COLOR: always

jobs:
  # 0) Move/force the "nightly" tag to main and delete the prior GH release.
  prepare-nightly-release:
    name: Prepare nightly tag + delete prior release
    runs-on: ubuntu-latest
    permissions:
      contents: write # push nightly tag and delete prior release
    env:
      GH_TOKEN: ${{ github.token }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Set nightly tag to latest main
        shell: bash
        run: |
          set -euo pipefail
          git fetch origin main
          MAIN_SHA="$(git rev-parse origin/main)"

          # Move the nightly tag via the API (git push is unavailable with persist-credentials: false)
          gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/nightly" \
            --method PATCH \
            --field sha="${MAIN_SHA}" \
            --field force=true 2>/dev/null \
          || gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
            --method POST \
            --field ref="refs/tags/nightly" \
            --field sha="${MAIN_SHA}"

      - name: Delete existing nightly release (if any)
        shell: bash
        run: gh release delete nightly --yes || true

  # 1) Build & push per-arch nightly images, sign each by digest, export digests for manifest job.
  build-oci-images:
    name: Build OCI nightly image (${{ matrix.arch }})
    environment: staging
    needs: prepare-nightly-release
    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 GHCR
        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:nightly-${{ matrix.arch }}
          cache-from: type=gha,scope=hyper-mcp-remote-nightly-${{ matrix.arch }}
          cache-to: type=gha,mode=max,scope=hyper-mcp-remote-nightly-${{ 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 nightly 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}:nightly-${ARCH} -> ${DIGEST}"
        env:
          STEPS_BUILD_OUTPUTS_DIGEST: ${{ steps.build.outputs.digest }}
          REPOSITORY_OWNER: ${{ github.repository_owner }}
          ARCH: ${{ matrix.arch }}

  # 2) Create multi-arch nightly manifest (tag=nightly), sign manifest by digest, export digest for release notes.
  create-multiarch-manifests:
    name: Create & sign multi-arch nightly manifest
    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:
      nightly_digest: ${{ steps.digest.outputs.nightly_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 GHCR
        uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

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

          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

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

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

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

          echo "nightly_digest=${NIGHTLY_DIGEST}" >> "$GITHUB_OUTPUT"
          echo "Resolved ${IMAGE}:nightly -> ${NIGHTLY_DIGEST}"

          echo "Signing nightly multi-arch manifest by digest: ${IMAGE}@${NIGHTLY_DIGEST}"
          cosign sign --yes "${IMAGE}@${NIGHTLY_DIGEST}"
        env:
          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 }}
          REPOSITORY_OWNER: ${{ github.repository_owner }}

  # 3) Build binaries (matrix), upload as artifacts.
  build-nightly-binaries:
    name: Build nightly binaries (${{ matrix.target }})
    environment: staging
    needs: prepare-nightly-release
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-24.04
            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 compilation 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
        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: nightly-${{ 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) Create the GitHub nightly release once, attach all artifacts, and include the immutable nightly digest.
  publish-nightly-release:
    name: Publish nightly GitHub Release
    needs:
      - build-nightly-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: nightly-*
          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 notes (with immutable digest)
        id: notes
        shell: bash
        run: |
          set -euo pipefail

          IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp-remote"
          NIGHTLY_DIGEST="${NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_NIGHTLY_DIGEST}"
          SHA="${GITHUB_SHA}"

          cat > release-body.md <<EOF
          Nightly build from \`main\`.

          Commit:
          - \`${SHA}\`

          Container image (tag):
          - \`${IMAGE}:nightly\`

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

          All container images are signed with Cosign. Verify the **immutable digest** with:

          \`\`\`bash
          cosign verify \
            --certificate-identity "https://github.com/hyper-mcp-rs/hyper-mcp-remote/.github/workflows/nightly.yml@refs/heads/main" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            ${IMAGE}@${NIGHTLY_DIGEST}
          \`\`\`
          EOF
        env:
          NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_NIGHTLY_DIGEST: ${{ needs.create-multiarch-manifests.outputs.nightly_digest }}
          REPOSITORY_OWNER: ${{ github.repository_owner }}

      - name: Create nightly release
        shell: bash
        run: |
          gh release create nightly \
            --title "Nightly build" \
            --prerelease \
            --generate-notes \
            --notes-file release-body.md \
            out/* \
            sbom.cdx.json
