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:
  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@v7
        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 --abbrev=0: $(git describe --tags --abbrev=0)"
          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 describe --tags --abbrev=0 2>/dev/null || true)
          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@v7

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v9
        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

      - name: Run go generate
        run: go generate ./...
      - name: Verify no changes from go generate
        run: |
          git status --porcelain
          if [ -n "$(git status --porcelain)" ]; then
            echo 'Generated code is out of date. Run "make gen" and commit the changes'
            exit 1
          fi

  validate-migrations:
    name: Validate DB migrations
    runs-on: ubuntu-latest
    # PR-only gate is at step level: a job-level skip would propagate through
    # the needs chain (actions/runner#491) and skip all release jobs on tag pushes.
    steps:
      - uses: actions/checkout@v7
        if: github.event_name == 'pull_request'
        with:
          fetch-depth: 0
      # Refresh the base branch so the check compares against its CURRENT tip,
      # not the (possibly stale) commit the PR was opened against.
      - name: Fetch latest base branch
        if: github.event_name == 'pull_request'
        run: git fetch --no-tags origin "+refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
      - name: Validate migration ordering and naming
        if: github.event_name == 'pull_request'
        env:
          BASE_REF: origin/${{ github.event.pull_request.base.ref }}
        run: ./.github/workflows/validate-migrations.sh

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

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - name: Download dependencies
        run: go mod download

      - name: Test
        run: go test -shuffle=on -tags netgo,sqlite_fts5 -race ./... -v

      - name: Test ndpgen
        run: |
          cd plugins/cmd/ndpgen
          go test -shuffle=on -v
          go build -o ndpgen .
          ./ndpgen --help

  go-windows:
    name: Test Go code (Windows)
    runs-on: windows-2022
    env:
      FFMPEG_VERSION: "7.1"
      FFMPEG_REPOSITORY: navidrome/ffmpeg-windows-builds
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - uses: msys2/setup-msys2@v2
        with:
          msystem: MINGW64
          install: mingw-w64-x86_64-gcc
          update: false

      - name: Add mingw64 to PATH
        shell: bash
        run: echo "C:/msys64/mingw64/bin" >> $GITHUB_PATH

      - name: Cache ffmpeg
        id: ffmpeg-cache
        uses: actions/cache@v6
        with:
          path: C:\ffmpeg
          key: ffmpeg-${{ env.FFMPEG_VERSION }}-win64

      - name: Download ffmpeg
        if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
        shell: pwsh
        run: |
          $asset = "ffmpeg-n${env:FFMPEG_VERSION}-latest-win64-gpl-${env:FFMPEG_VERSION}"
          $url = "https://github.com/${env:FFMPEG_REPOSITORY}/releases/download/latest/$asset.zip"
          Invoke-WebRequest -Uri $url -OutFile ffmpeg.zip
          Expand-Archive ffmpeg.zip -DestinationPath C:\ffmpeg-extracted
          New-Item -ItemType Directory -Force -Path C:\ffmpeg\bin | Out-Null
          Copy-Item "C:\ffmpeg-extracted\$asset\bin\ffmpeg.exe"  C:\ffmpeg\bin
          Copy-Item "C:\ffmpeg-extracted\$asset\bin\ffprobe.exe" C:\ffmpeg\bin

      - name: Add ffmpeg to PATH
        shell: bash
        run: echo "C:/ffmpeg/bin" >> $GITHUB_PATH

      - name: Verify toolchain
        shell: pwsh
        run: |
          go version
          where.exe gcc
          gcc --version
          ffmpeg -version
          ffprobe -version

      - name: Download dependencies
        shell: bash
        run: go mod download

      - name: Test
        shell: bash
        env:
          CGO_ENABLED: "1"
        run: go test -shuffle=on -tags netgo,sqlite_fts5 ./... -v

      - name: Test ndpgen
        shell: pwsh
        run: |
          cd plugins\cmd\ndpgen
          go test -shuffle=on -v
          go build -o ndpgen.exe .
          .\ndpgen.exe --help

  js:
    name: Test JS code
    runs-on: ubuntu-latest
    env:
      NODE_OPTIONS: "--max_old_space_size=4096"
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          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@v7
      - 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-windows, go-lint, i18n-lint, git-version, check-push-enabled, validate-migrations]
    strategy:
      matrix:
        platform: [ linux/amd64, linux/arm64, linux/arm/v5, linux/arm/v6, linux/arm/v7, linux/386, linux/riscv64, 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@v7

      - 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@v7
        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 }}

      - name: Set up QEMU for smoke test
        if: env.IS_LINUX == 'true'
        uses: docker/setup-qemu-action@v4

      # The binary is static, so binfmt+qemu runs it directly on the runner.
      # Catches startup crashes in cross-compiled binaries before they ship,
      # e.g. the broken ifunc relocations on 32-bit arm from issue #5738.
      - name: Smoke-test binary
        if: env.IS_LINUX == 'true'
        run: |
          BIN=./output/${{ env.PLATFORM }}/navidrome
          chmod +x "$BIN"
          "$BIN" --help >/dev/null
          echo "OK: ${{ matrix.platform }} binary starts"

      - name: Upload Binaries
        uses: actions/upload-artifact@v7
        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@v7
        with:
          context: .
          file: Dockerfile
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.docker.outputs.labels }}
          build-args: |
            GIT_SHA=${{ env.GIT_SHA }}
            GIT_TAG=${{ env.GIT_TAG }}
          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@v7
        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-ghcr:
    name: Push to GHCR
    permissions:
      contents: read
      packages: write
    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@v7

      - name: Download digests
        uses: actions/download-artifact@v8
        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 }}

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

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

  push-manifest-dockerhub:
    name: Push to Docker Hub
    runs-on: ubuntu-latest
    permissions:
      contents: read
    needs: [build, check-push-enabled]
    if: needs.check-push-enabled.outputs.is_enabled == 'true' && vars.DOCKER_HUB_REPO != ''
    continue-on-error: true
    steps:
      - uses: actions/checkout@v7

      - name: Download digests
        uses: actions/download-artifact@v8
        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 Docker Hub
        uses: nick-fields/retry@v4
        with:
          timeout_minutes: 5
          max_attempts: 3
          retry_wait_seconds: 30
          command: |
            cd /tmp/digests
            docker buildx imagetools create $(jq -cr '.tags | map(select(startswith("ghcr.io") | not)) | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
              $(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)

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

  cleanup-digests:
    name: Cleanup digest artifacts
    runs-on: ubuntu-latest
    needs: [push-manifest-ghcr, push-manifest-dockerhub]
    if: always() && needs.push-manifest-ghcr.result == 'success'
    steps:
      - 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@v7

      - uses: actions/download-artifact@v8
        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@v7
        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@v7
        with:
          fetch-depth: 0
          fetch-tags: true

      - uses: actions/download-artifact@v8
        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@v7
        with:
          version: '2.16.0'
          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@v7
        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@v8
        with:
          name: packages
          path: ./dist

      - name: Upload all-packages artifact
        uses: actions/upload-artifact@v7
        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