name: Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  packages: write

# Serialize releases: a later tag must not race an in-flight publish and win
# the npm `latest` dist-tag. Never cancel a publish mid-flight.
concurrency:
  group: release
  cancel-in-progress: false

env:
  REGISTRY_GHCR: ghcr.io
  REGISTRY_DOCKER: docker.io
  IMAGE_GHCR: ghcr.io/linagora/ldap-rest
  IMAGE_DOCKER: docker.io/yadd/ldap-rest

jobs:
  # Gate the whole release on a green build + test suite. Every publish job
  # (docker, helm, npm) depends on this, so nothing is published if tests fail.
  test:
    name: Test
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      # Build before testing: the suite imports the compiled plugins from dist/.
      - name: Build
        run: npm run build

      - name: Run tests
        run: npm test

  # Build and push the amd64 image (fast, runs first).
  docker-amd64:
    name: Build & Push Docker (amd64)
    runs-on: ubuntu-latest
    needs: test
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Extract version from tag
        id: version
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "Version: $VERSION"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_GHCR }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_DOCKER }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.IMAGE_GHCR }}
            ${{ env.IMAGE_DOCKER }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}

      - name: Build and push (amd64)
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          push: true
          tags: |
            ${{ env.IMAGE_GHCR }}:${{ steps.version.outputs.version }}-amd64
            ${{ env.IMAGE_DOCKER }}:${{ steps.version.outputs.version }}-amd64
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # Build the arm64 image (slow; runs in parallel with the helm job).
  docker-arm64:
    name: Build & Push Docker (arm64)
    runs-on: ubuntu-latest
    needs: docker-amd64
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_GHCR }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_DOCKER }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push (arm64)
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: |
            ${{ env.IMAGE_GHCR }}:${{ needs.docker-amd64.outputs.version }}-arm64
            ${{ env.IMAGE_DOCKER }}:${{ needs.docker-amd64.outputs.version }}-arm64
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # Assemble the multi-arch manifests once both arch builds are pushed.
  docker-manifest:
    name: Create Multi-arch Manifest
    runs-on: ubuntu-latest
    needs: [docker-amd64, docker-arm64]
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_GHCR }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_DOCKER }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Create and push multi-arch manifests
        env:
          VERSION: ${{ needs.docker-amd64.outputs.version }}
        run: |
          MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
          MAJOR=$(echo "$VERSION" | cut -d. -f1)

          for IMAGE in "$IMAGE_GHCR" "$IMAGE_DOCKER"; do
            for TAG in "$VERSION" "$MAJOR_MINOR" "$MAJOR" "latest"; do
              docker buildx imagetools create -t "${IMAGE}:${TAG}" \
                "${IMAGE}:${VERSION}-amd64" \
                "${IMAGE}:${VERSION}-arm64"
            done
          done

  # Package and push the Helm chart as an OCI artifact (parallel with arm64).
  helm:
    name: Release Helm Chart
    runs-on: ubuntu-latest
    needs: docker-amd64
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Helm
        uses: azure/setup-helm@v4
        with:
          version: v3.16.0

      - name: Set chart version from tag
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          echo "Setting chart version and appVersion to $VERSION"
          sed -i "s/^version:.*/version: $VERSION/" helm/ldap-rest/Chart.yaml
          sed -i "s/^appVersion:.*/appVersion: \"$VERSION\"/" helm/ldap-rest/Chart.yaml
          cat helm/ldap-rest/Chart.yaml

      - name: Lint chart
        run: helm lint helm/ldap-rest

      - name: Package chart
        run: helm package helm/ldap-rest --destination .

      - name: Login to GHCR (Helm registry)
        run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ${{ env.REGISTRY_GHCR }} --username ${{ github.actor }} --password-stdin

      - name: Push chart to OCI registry
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          helm push "ldap-rest-${VERSION}.tgz" oci://${{ env.REGISTRY_GHCR }}/linagora/charts

  # Publish the package to the npm registry (gated on the test job).
  npm:
    name: Publish to npm
    runs-on: ubuntu-latest
    needs: test
    timeout-minutes: 15
    # id-token: write authenticates to npm through OIDC (Trusted Publishing):
    # no long-lived NPM_TOKEN to store or rotate, and a provenance attestation
    # is attached to every release for free.
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22.x
          cache: 'npm'
      # registry-url is intentionally omitted: it makes setup-node write an
      # .npmrc carrying a dummy auth token that can shadow OIDC. npmjs.org is
      # already the default registry, so OIDC publishing works without it.

      # Trusted Publishing requires npm >= 11.5.1, newer than the version
      # bundled with Node 22.
      - name: Update npm for Trusted Publishing
        run: npm install -g npm@latest

      - name: Install dependencies
        run: npm ci

      # Tests already ran in the `test` job; here we only build the artifacts
      # needed to pack and publish (dist/ + entry-point files).
      - name: Build package
        run: npm run build

      - name: Verify tag matches package.json version
        run: |
          TAG="${GITHUB_REF_NAME#v}"
          PKG="$(node -p "require('./package.json').version")"
          if [ "$TAG" != "$PKG" ]; then
            echo "Tag v$TAG does not match package.json version $PKG."
            echo "Bump the version in package.json before pushing the tag."
            exit 1
          fi
          echo "Tag matches package.json version: $PKG"

      - name: Verify package entry points exist
        run: |
          node -e '
            const fs = require("fs");
            const p = require("./package.json");
            const files = new Set();
            for (const v of Object.values(p.exports || {})) {
              if (v && typeof v === "object") {
                if (v.import) files.add(v.import);
                if (v.types) files.add(v.types);
              }
            }
            for (const b of Object.values(p.bin || {})) files.add(b);
            const missing = [...files].filter((f) => !fs.existsSync(f));
            if (missing.length) {
              console.error("Missing entry files:\n  " + missing.join("\n  "));
              process.exit(1);
            }
            console.log("Verified " + files.size + " package entry files");
          '

      - name: Pack tarball
        run: npm pack

      - name: Publish to npm
        run: |
          # Keep prereleases (e.g. v1.2.3-rc.1) off the `latest` dist-tag.
          case "$GITHUB_REF_NAME" in
            *-*) NPM_TAG=next ;;
            *)   NPM_TAG=latest ;;
          esac
          npm publish --access public --tag "$NPM_TAG"

      - name: Upload release tarball
        uses: actions/upload-artifact@v4
        with:
          name: npm-tarball
          path: ldap-rest-*.tgz
          if-no-files-found: error

  # Cut a GitHub Release, gated on a successful npm publish so the two never
  # disagree. Notes are auto-generated and the published tarball is attached.
  release:
    name: Create GitHub Release
    needs: npm
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: write
    steps:
      - name: Download release tarball
        uses: actions/download-artifact@v4
        with:
          name: npm-tarball

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          fail_on_unmatched_files: true
          prerelease: ${{ contains(github.ref_name, '-') }}
          files: ldap-rest-*.tgz
