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.0.2-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)" 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 GIT_TAG=${GIT_TAG}-SNAPSHOT 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="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@v6 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$'` - 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 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_v* - id: set-package-list name: Export list of generated packages run: | cd dist set +x ITEMS=$(ls navidrome_v* | sed 's/^navidrome_v[^_]*_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_v*_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