name: "Pipeline: Test, Lint, Build"
on:
  push:
    branches:
      - master
    tags:
      - "v*"
  pull_request:
    branches:
      - master

concurrency:
  group: ${{ startsWith(github.ref, 'refs/tags/v') && 'tag' || 'branch' }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CROSS_TAGLIB_VERSION: "2.1.1-1"
  IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }}

jobs:
  git-version:
    name: Get version info
    runs-on: ubuntu-latest
    outputs:
      git_tag: ${{ steps.git-version.outputs.GIT_TAG }}
      git_sha: ${{ steps.git-version.outputs.GIT_SHA }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: Show git version info
        run: |
          echo "git describe (dirty): $(git describe --dirty --always --tags)"
          echo "git describe --tags: $(git describe --tags `git rev-list --tags --max-count=1`)"
          echo "git tag: $(git tag --sort=-committerdate | head -n 1)"
          echo "github_ref: $GITHUB_REF"
          echo "github_head_sha: ${{ github.event.pull_request.head.sha }}"
          git tag -l
      - name: Determine git current SHA and latest tag
        id: git-version
        run: |
          GIT_TAG=$(git tag --sort=-committerdate | head -n 1)
          if [ -n "$GIT_TAG" ]; then
            if [[ "$GITHUB_REF" != refs/tags/* ]]; then
              GIT_TAG=${GIT_TAG}-SNAPSHOT
            fi
            echo "GIT_TAG=$GIT_TAG" >> $GITHUB_OUTPUT
          fi
          GIT_SHA=$(git rev-parse --short HEAD)
          PR_NUM=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
          if [[ $PR_NUM != "null" ]]; then
            GIT_SHA=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-8)
            GIT_SHA="pr-${PR_NUM}/${GIT_SHA}"
          fi
          echo "GIT_SHA=$GIT_SHA" >> $GITHUB_OUTPUT
          
          echo "GIT_TAG=$GIT_TAG"
          echo "GIT_SHA=$GIT_SHA"

  go-lint:
    name: Lint Go code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download TagLib
        uses: ./.github/actions/download-taglib
        with:
          version: ${{ env.CROSS_TAGLIB_VERSION }}

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v8
        with:
          version: latest
          problem-matchers: true
          args: --timeout 2m

      - name: Run go goimports
        run: go run golang.org/x/tools/cmd/goimports@latest -w `find . -name '*.go' | grep -v '_gen.go$' | grep -v '.pb.go$'`
      - run: go mod tidy
      - name: Verify no changes from goimports and go mod tidy
        run: |
          git status --porcelain
          if [ -n "$(git status --porcelain)" ]; then
            echo 'To fix this check, run "make format" and commit the changes'
            exit 1
          fi

  go:
    name: Test Go code
    runs-on: ubuntu-latest
    steps:
      - name: Check out code into the Go module directory
        uses: actions/checkout@v4

      - name: Download TagLib
        uses: ./.github/actions/download-taglib
        with:
          version: ${{ env.CROSS_TAGLIB_VERSION }}

      - name: Download dependencies
        run: go mod download

      - name: Test
        run: |
          pkg-config --define-prefix --cflags --libs taglib # for debugging
          go test -shuffle=on -tags netgo -race -cover ./... -v

  js:
    name: Test JS code
    runs-on: ubuntu-latest
    env:
      NODE_OPTIONS: "--max_old_space_size=4096"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
          cache-dependency-path: "**/package-lock.json"

      - name: npm install dependencies
        run: |
          cd ui
          npm ci

      - name: npm lint
        run: |
          cd ui
          npm run check-formatting && npm run lint

      - name: npm test
        run: |
          cd ui
          npm test

      - name: npm build
        run: |
          cd ui
          npm run build

  i18n-lint:
    name: Lint i18n files
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          set -e
          for file in resources/i18n/*.json; do
            echo "Validating $file"
            if ! jq empty "$file" 2>error.log; then
              error_message=$(cat error.log)
              line_number=$(echo "$error_message" | grep -oP 'line \K[0-9]+')
              echo "::error file=$file,line=$line_number::$error_message"
              exit 1
            fi
          done
      - run: ./.github/workflows/validate-translations.sh -v


  check-push-enabled:
    name: Check Docker configuration
    runs-on: ubuntu-latest
    outputs:
      is_enabled: ${{ steps.check.outputs.is_enabled }}
    steps:
      - name: Check if Docker push is configured
        id: check
        run: echo "is_enabled=${{ secrets.DOCKER_HUB_USERNAME != '' }}" >> $GITHUB_OUTPUT

  build:
    name: Build
    needs: [js, go, go-lint, i18n-lint, git-version, check-push-enabled]
    strategy:
      matrix:
        platform: [ linux/amd64, linux/arm64, linux/arm/v5, linux/arm/v6, linux/arm/v7, linux/386, darwin/amd64, darwin/arm64, windows/amd64, windows/386 ]
    runs-on: ubuntu-latest
    env:
      IS_LINUX: ${{ startsWith(matrix.platform, 'linux/') && 'true' || 'false' }}
      IS_ARMV5: ${{ matrix.platform == 'linux/arm/v5' && 'true' || 'false' }}
      IS_DOCKER_PUSH_CONFIGURED: ${{ needs.check-push-enabled.outputs.is_enabled == 'true' }}
      DOCKER_BUILD_SUMMARY: false
      GIT_SHA: ${{ needs.git-version.outputs.git_sha }}
      GIT_TAG: ${{ needs.git-version.outputs.git_tag }}
    steps:
      - name: Sanitize platform name
        id: set-platform
        run: |
          PLATFORM=$(echo ${{ matrix.platform }} | tr '/' '_')
          echo "PLATFORM=$PLATFORM" >> $GITHUB_ENV

      - uses: actions/checkout@v4

      - name: Prepare Docker Buildx
        uses: ./.github/actions/prepare-docker
        id: docker
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          hub_repository: ${{ vars.DOCKER_HUB_REPO }}
          hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
          hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }}

      - name: Build Binaries
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile
          platforms: ${{ matrix.platform }}
          outputs: |
            type=local,dest=./output/${{ env.PLATFORM }}
          target: binary
          build-args: |
            GIT_SHA=${{ env.GIT_SHA }}
            GIT_TAG=${{ env.GIT_TAG }}
            CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }}

      - name: Upload Binaries
        uses: actions/upload-artifact@v4
        with:
          name: navidrome-${{ env.PLATFORM }}
          path: ./output
          retention-days: 7

      - name: Build and push image by digest
        id: push-image
        if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false'
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.docker.outputs.labels }}
          build-args: |
            GIT_SHA=${{ env.GIT_SHA }}
            GIT_TAG=${{ env.GIT_TAG }}
            CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }}
          outputs: |
            type=image,name=${{ steps.docker.outputs.hub_repository }},push-by-digest=true,name-canonical=true,push=${{ steps.docker.outputs.hub_enabled }}
            type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true

      - name: Export digest
        if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false'
        run: |
          mkdir -p /tmp/digests
          digest="${{ steps.push-image.outputs.digest }}"
          touch "/tmp/digests/${digest#sha256:}"          

      - name: Upload digest
        uses: actions/upload-artifact@v4
        if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false'
        with:
          name: digests-${{ env.PLATFORM }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  push-manifest:
    name: Push Docker manifest
    runs-on: ubuntu-latest
    needs: [build, check-push-enabled]
    if: needs.check-push-enabled.outputs.is_enabled == 'true'
    env:
      REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
    steps:
      - uses: actions/checkout@v4

      - name: Download digests
        uses: actions/download-artifact@v4
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true

      - name: Prepare Docker Buildx
        uses: ./.github/actions/prepare-docker
        id: docker
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          hub_repository: ${{ vars.DOCKER_HUB_REPO }}
          hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
          hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }}

      - name: Create manifest list and push to ghcr.io
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)          

      - name: Create manifest list and push to Docker Hub
        working-directory: /tmp/digests
        if: vars.DOCKER_HUB_REPO != ''
        run: |
          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf '${{ vars.DOCKER_HUB_REPO }}@sha256:%s ' *)          

      - name: Inspect image in ghcr.io
        run: |
          docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.docker.outputs.version }}

      - name: Inspect image in Docker Hub
        if: vars.DOCKER_HUB_REPO != ''
        run: |
          docker buildx imagetools inspect ${{ vars.DOCKER_HUB_REPO }}:${{ steps.docker.outputs.version }}

      - name: Delete unnecessary digest artifacts
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          for artifact in $(gh api repos/${{ github.repository }}/actions/artifacts | jq -r '.artifacts[] | select(.name | startswith("digests-")) | .id'); do
            gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact
          done

  msi:
    name: Build Windows installers
    needs: [build, git-version]
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          path: ./binaries
          pattern: navidrome-windows*
          merge-multiple: true

      - name: Install Wix
        run: sudo apt-get install -y wixl jq

      - name: Build MSI
        env:
          GIT_TAG: ${{ needs.git-version.outputs.git_tag }}
        run: |
          rm -rf binaries/msi
          sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} 386
          sudo GIT_TAG=$GIT_TAG release/wix/build_msi.sh ${GITHUB_WORKSPACE} amd64
          du -h binaries/msi/*.msi

      - name: Upload MSI files
        uses: actions/upload-artifact@v4
        with:
          name: navidrome-windows-installers
          path: binaries/msi/*.msi
          retention-days: 7

  release:
    name: Package/Release
    needs: [build, msi]
    runs-on: ubuntu-latest
    outputs:
      package_list: ${{ steps.set-package-list.outputs.package_list }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true

      - uses: actions/download-artifact@v4
        with:
          path: ./binaries
          pattern: navidrome-*
          merge-multiple: true

      - run: ls -lR ./binaries

      - name: Set RELEASE_FLAGS for snapshot releases
        if: env.IS_RELEASE == 'false'
        run: echo 'RELEASE_FLAGS=--skip=publish --snapshot' >> $GITHUB_ENV

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          version: '~> v2'
          args: "release --clean -f release/goreleaser.yml ${{ env.RELEASE_FLAGS }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Remove build artifacts
        run: |
          ls -l ./dist
          rm ./dist/*.tar.gz ./dist/*.zip

      - name: Upload all-packages artifact
        uses: actions/upload-artifact@v4
        with:
          name: packages
          path: dist/navidrome_0*

      - id: set-package-list
        name: Export list of generated packages
        run: |
          cd dist
          set +x
          ITEMS=$(ls navidrome_0* | sed 's/^navidrome_0[^_]*_linux_//' | jq -R -s -c 'split("\n")[:-1]')
          echo $ITEMS
          echo "package_list=${ITEMS}" >> $GITHUB_OUTPUT

  upload-packages:
    name: Upload Linux PKG
    runs-on: ubuntu-latest
    needs: [release]
    strategy:
      matrix:
        item: ${{ fromJson(needs.release.outputs.package_list) }}
    steps:
      - name: Download all-packages artifact
        uses: actions/download-artifact@v4
        with:
          name: packages
          path: ./dist

      - name: Upload all-packages artifact
        uses: actions/upload-artifact@v4
        with:
          name: navidrome_linux_${{ matrix.item }}
          path: dist/navidrome_0*_linux_${{ matrix.item }}

#  delete-artifacts:
#    name: Delete unused artifacts
#    runs-on: ubuntu-latest
#    needs: [upload-packages]
#    steps:
#      - name: Delete all-packages artifact
#        env:
#          GH_TOKEN: ${{ github.token }}
#        run: |
#          for artifact in $(gh api repos/${{ github.repository }}/actions/artifacts | jq -r '.artifacts[] | select(.name | startswith("packages")) | .id'); do
#            gh api --method DELETE repos/${{ github.repository }}/actions/artifacts/$artifact
#          done