From d47b71b2f1ba7969a7c5b44625f91e37214878d7 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 19:36:55 -0400 Subject: [PATCH 01/35] feat(ci): migrate CI/CD from Azure Pipelines to GitHub Actions (#18) Add monolith GitHub Actions workflow with 17 jobs covering build, test, packaging, analysis, and notifications. Disable Azure Pipelines triggers while preserving the file for reference. Jobs: setup (git tag counter), build-backend (cross-compile all RIDs), build-frontend, packages (13 archives), lint, unit/integration tests (native + docker + postgres), Windows installer, Sentry source map upload, SonarCloud analysis (frontend + backend), API docs generation, and Discord notifications. --- .github/workflows/ci.yml | 974 +++++++++++++++++++++++++++++++++++++++ azure-pipelines.yml | 42 +- 2 files changed, 998 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..ac39f76834f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,974 @@ +name: CI + +on: + push: + branches: + - develop + - master + paths-ignore: + - '.github/**' + - 'src/Radarr.Api.*/openapi.json' + pull_request: + branches: + - develop + paths-ignore: + - '.github/**' + - 'src/NzbDrone.Core/Localization/Core/**' + - 'src/Radarr.Api.*/openapi.json' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MAJOR_VERSION: '6.1.1' + DOTNET_VERSION: '8.0.405' + NODE_VERSION: '20.11.1' + INNO_VERSION: '6.2.2' + +jobs: + # --------------------------------------------------------------------------- + # Setup: generate build number, detect backend changes + # --------------------------------------------------------------------------- + setup: + runs-on: ubuntu-24.04 + outputs: + build_number: ${{ steps.counter.outputs.build_number }} + radarr_version: ${{ steps.counter.outputs.radarr_version }} + branch_name: ${{ steps.counter.outputs.branch_name }} + backend_changed: ${{ steps.changes.outputs.backend_changed }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate build number + id: counter + env: + MAJOR_VERSION: ${{ env.MAJOR_VERSION }} + run: | + git fetch --tags --force + LAST=$(git tag -l 'build-counter-*' | sed 's/build-counter-//' | sort -n | tail -1) + NEXT=$(( ${LAST:-2999} + 1 )) + echo "build_number=$NEXT" >> "$GITHUB_OUTPUT" + echo "radarr_version=${MAJOR_VERSION}.${NEXT}" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "branch_name=${{ github.head_ref }}" >> "$GITHUB_OUTPUT" + else + echo "branch_name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + + - name: Push build counter tag + if: github.event_name != 'pull_request' + env: + BUILD_NUMBER: ${{ steps.counter.outputs.build_number }} + run: | + for attempt in 1 2 3; do + if git tag "build-counter-${BUILD_NUMBER}" && git push origin "build-counter-${BUILD_NUMBER}"; then + echo "Tag pushed successfully on attempt $attempt" + break + fi + echo "Tag push failed on attempt $attempt, retrying..." + git fetch --tags --force + LAST=$(git tag -l 'build-counter-*' | sed 's/build-counter-//' | sort -n | tail -1) + BUILD_NUMBER=$(( LAST + 1 )) + echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT" + echo "radarr_version=${MAJOR_VERSION}.${BUILD_NUMBER}" >> "$GITHUB_OUTPUT" + done + + - name: Detect backend changes + id: changes + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + git diff origin/develop...HEAD --name-only | grep -qE "^(src/|azure-pipelines.yml)" \ + && echo "backend_changed=true" >> "$GITHUB_OUTPUT" \ + || echo "backend_changed=false" >> "$GITHUB_OUTPUT" + else + echo "backend_changed=true" >> "$GITHUB_OUTPUT" + fi + + # --------------------------------------------------------------------------- + # Build Backend: cross-compile all RIDs from a single Linux runner + # --------------------------------------------------------------------------- + build-backend: + runs-on: ubuntu-24.04 + needs: setup + env: + RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} + BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 1 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} + restore-keys: nuget-${{ runner.os }}- + + - name: Enable FreeBSD platform in SDK + run: | + BUNDLED="${DOTNET_ROOT}/sdk/${DOTNET_VERSION}/Microsoft.NETCoreSdk.BundledVersions.props" + echo "Patching: $BUNDLED" + if grep -q freebsd-x64 "$BUNDLED"; then + echo "Extra platforms already enabled" + else + sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLED" + fi + + - name: Build backend + run: ./build.sh --backend --enable-extra-platforms + + - name: Clean intermediate output + run: | + find _output -type f ! -path "*/publish/*" -exec rm -rf {} \; + find _output -depth -empty -type d -exec rm -r "{}" \; + find _tests -type f ! -path "*/publish/*" -exec rm -rf {} \; + find _tests -depth -empty -type d -exec rm -r "{}" \; + + - name: Upload backend output + uses: actions/upload-artifact@v4 + with: + name: backend-output + path: _output/ + compression-level: 1 + + - name: Upload win-x64 tests + uses: actions/upload-artifact@v4 + with: + name: win-x64-tests + path: _tests/net8.0/win-x64/publish/ + + - name: Upload linux-x64 tests + uses: actions/upload-artifact@v4 + with: + name: linux-x64-tests + path: _tests/net8.0/linux-x64/publish/ + + - name: Upload linux-musl-x64 tests + uses: actions/upload-artifact@v4 + with: + name: linux-musl-x64-tests + path: _tests/net8.0/linux-musl-x64/publish/ + + - name: Upload osx-x64 tests + uses: actions/upload-artifact@v4 + with: + name: osx-x64-tests + path: _tests/net8.0/osx-x64/publish/ + + # --------------------------------------------------------------------------- + # Build Frontend + # --------------------------------------------------------------------------- + build-frontend: + runs-on: ubuntu-24.04 + needs: setup + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache Yarn packages + uses: actions/cache@v4 + with: + path: ~/.cache/yarn + key: yarn-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + restore-keys: yarn-${{ runner.os }}- + + - name: Build frontend + run: ./build.sh --frontend + env: + FORCE_COLOR: '0' + YARN_CACHE_FOLDER: ~/.cache/yarn + + - name: Upload frontend output + uses: actions/upload-artifact@v4 + with: + name: frontend-output + path: _output/UI/ + + # --------------------------------------------------------------------------- + # Packages: create archives for all platforms + # --------------------------------------------------------------------------- + packages: + runs-on: ubuntu-24.04 + needs: [build-backend, build-frontend, setup] + env: + RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} + BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} + BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download backend output + uses: actions/download-artifact@v4 + with: + name: backend-output + path: _output/ + + - name: Download frontend output + uses: actions/download-artifact@v4 + with: + name: frontend-output + path: _output/UI/ + + - name: Create packages + run: ./build.sh --packages --enable-extra-platforms + + - name: Set executable bits + run: | + find . -name "ffprobe" -exec chmod a+x {} \; + find . -name "Radarr" -exec chmod a+x {} \; + find . -name "Radarr.Update" -exec chmod a+x {} \; + + - name: Create archives + run: | + mkdir -p _archives + + # Windows zips + cd _artifacts/win-x64/net8.0 && zip -r ../../../_archives/Radarr.${BUILDNAME}.windows-core-x64.zip . && cd ../../.. + cd _artifacts/win-x86/net8.0 && zip -r ../../../_archives/Radarr.${BUILDNAME}.windows-core-x86.zip . && cd ../../.. + + # macOS app zips + cd _artifacts/osx-x64-app/net8.0 && zip -r ../../../_archives/Radarr.${BUILDNAME}.osx-app-core-x64.zip . && cd ../../.. + cd _artifacts/osx-arm64-app/net8.0 && zip -r ../../../_archives/Radarr.${BUILDNAME}.osx-app-core-arm64.zip . && cd ../../.. + + # macOS tars + tar -czf _archives/Radarr.${BUILDNAME}.osx-core-x64.tar.gz -C _artifacts/osx-x64/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.osx-core-arm64.tar.gz -C _artifacts/osx-arm64/net8.0 . + + # Linux tars + tar -czf _archives/Radarr.${BUILDNAME}.linux-core-x64.tar.gz -C _artifacts/linux-x64/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.linux-musl-core-x64.tar.gz -C _artifacts/linux-musl-x64/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.linux-core-arm.tar.gz -C _artifacts/linux-arm/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.linux-musl-core-arm.tar.gz -C _artifacts/linux-musl-arm/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.linux-core-arm64.tar.gz -C _artifacts/linux-arm64/net8.0 . + tar -czf _archives/Radarr.${BUILDNAME}.linux-musl-core-arm64.tar.gz -C _artifacts/linux-musl-arm64/net8.0 . + + # FreeBSD tar + tar -czf _archives/Radarr.${BUILDNAME}.freebsd-core-x64.tar.gz -C _artifacts/freebsd-x64/net8.0 . + + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: packages + path: _archives/ + + # --------------------------------------------------------------------------- + # Lint + # --------------------------------------------------------------------------- + lint: + runs-on: ubuntu-24.04 + needs: setup + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache Yarn packages + uses: actions/cache@v4 + with: + path: ~/.cache/yarn + key: yarn-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + restore-keys: yarn-${{ runner.os }}- + + - name: Lint + run: ./build.sh --lint + env: + FORCE_COLOR: '0' + YARN_CACHE_FOLDER: ~/.cache/yarn + + # --------------------------------------------------------------------------- + # Unit Tests: native platforms + # --------------------------------------------------------------------------- + unit-test-native: + needs: [build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + strategy: + fail-fast: false + matrix: + include: + - os: macos-15 + platform: Mac + artifact: osx-x64-tests + test-name: osx-x64 + - os: windows-2025 + platform: Windows + artifact: win-x64-tests + test-name: win-x64 + - os: ubuntu-24.04 + platform: Linux + artifact: linux-x64-tests + test-name: linux-x64 + runs-on: ${{ matrix.os }} + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: _tests/ + + - name: Enable Windows test service + if: matrix.platform == 'Windows' + run: Set-Service SCardSvr -StartupType Manual + shell: pwsh + + - name: Set executable permissions + if: matrix.platform != 'Windows' + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run unit tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh ${{ matrix.platform }} Unit Test + env: + TEST_DIR: ${{ github.workspace }}/_tests + shell: bash + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.test-name }}-unit-test-results + path: '**/TestResult.xml' + + - name: Report test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: ${{ matrix.test-name }} Unit Tests + path: '**/TestResult.xml' + reporter: java-junit + fail-on-error: true + + # --------------------------------------------------------------------------- + # Unit Tests: Docker (Alpine / musl) + # --------------------------------------------------------------------------- + unit-test-docker: + runs-on: ubuntu-24.04 + needs: [build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + container: + image: ghcr.io/servarr/testimages:alpine + timeout-minutes: 10 + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: linux-musl-x64-tests + path: _tests/ + + - name: Set executable permissions + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run unit tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh Linux Unit Test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: musl-unit-test-results + path: '**/TestResult.xml' + + # --------------------------------------------------------------------------- + # Unit Tests: Postgres + # --------------------------------------------------------------------------- + unit-test-postgres: + runs-on: ubuntu-24.04 + needs: [build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + strategy: + fail-fast: false + matrix: + postgres-version: ['14', '15'] + services: + postgres: + image: postgres:${{ matrix.postgres-version }} + env: + POSTGRES_USER: radarr + POSTGRES_PASSWORD: radarr + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + timeout-minutes: 10 + env: + Radarr__Postgres__Host: localhost + Radarr__Postgres__Port: '5432' + Radarr__Postgres__User: radarr + Radarr__Postgres__Password: radarr + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: linux-x64-tests + path: _tests/ + + - name: Set executable permissions + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run unit tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh Linux Unit Test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: postgres${{ matrix.postgres-version }}-unit-test-results + path: '**/TestResult.xml' + + # --------------------------------------------------------------------------- + # Integration Tests: native platforms + # --------------------------------------------------------------------------- + integration-native: + needs: [packages, build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + strategy: + fail-fast: false + matrix: + include: + - os: macos-15 + platform: Mac + artifact: osx-x64-tests + test-name: osx-x64 + pattern: 'Radarr.*.osx-core-x64.tar.gz' + extract-cmd: 'tar xzf' + - os: windows-2025 + platform: Windows + artifact: win-x64-tests + test-name: win-x64 + pattern: 'Radarr.*.windows-core-x64.zip' + extract-cmd: 'unzip' + - os: ubuntu-24.04 + platform: Linux + artifact: linux-x64-tests + test-name: linux-x64 + pattern: 'Radarr.*.linux-core-x64.tar.gz' + extract-cmd: 'tar xzf' + runs-on: ${{ matrix.os }} + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: _tests/ + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: packages + path: _packages/ + + - name: Extract package + run: | + mkdir -p _extracted + ARCHIVE=$(find _packages -name "${{ matrix.pattern }}" | head -1) + echo "Extracting: $ARCHIVE" + if [[ "$ARCHIVE" == *.zip ]]; then + unzip "$ARCHIVE" -d _extracted/ + else + tar xzf "$ARCHIVE" -C _extracted/ + fi + mkdir -p ./bin/ + cp -r _extracted/Radarr/. ./bin/ 2>/dev/null || cp -r _extracted/. ./bin/ + shell: bash + + - name: Enable Windows test service + if: matrix.platform == 'Windows' + run: Set-Service SCardSvr -StartupType Manual + shell: pwsh + + - name: Set executable permissions + if: matrix.platform != 'Windows' + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run integration tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh ${{ matrix.platform }} Integration Test + env: + TEST_DIR: ${{ github.workspace }}/_tests + shell: bash + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.test-name }}-integration-test-results + path: '**/TestResult.xml' + + # --------------------------------------------------------------------------- + # Integration Tests: Docker (Alpine / musl) + # --------------------------------------------------------------------------- + integration-docker: + runs-on: ubuntu-24.04 + needs: [packages, build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + container: + image: ghcr.io/servarr/testimages:alpine + timeout-minutes: 15 + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: linux-musl-x64-tests + path: _tests/ + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: packages + path: _packages/ + + - name: Extract package + run: | + mkdir -p _extracted + ARCHIVE=$(find _packages -name "Radarr.*.linux-musl-core-x64.tar.gz" | head -1) + tar xzf "$ARCHIVE" -C _extracted/ + mkdir -p ./bin/ + cp -r _extracted/Radarr/. ./bin/ 2>/dev/null || cp -r _extracted/. ./bin/ + + - name: Set executable permissions + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run integration tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh Linux Integration Test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: musl-integration-test-results + path: '**/TestResult.xml' + + # --------------------------------------------------------------------------- + # Integration Tests: Postgres + # --------------------------------------------------------------------------- + integration-postgres: + runs-on: ubuntu-24.04 + needs: [packages, build-backend, setup] + if: needs.setup.outputs.backend_changed == 'true' + strategy: + fail-fast: false + matrix: + postgres-version: ['14', '15'] + services: + postgres: + image: postgres:${{ matrix.postgres-version }} + env: + POSTGRES_USER: radarr + POSTGRES_PASSWORD: radarr + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + timeout-minutes: 15 + env: + Radarr__Postgres__Host: localhost + Radarr__Postgres__Port: '5432' + Radarr__Postgres__User: radarr + Radarr__Postgres__Password: radarr + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download test artifact + uses: actions/download-artifact@v4 + with: + name: linux-x64-tests + path: _tests/ + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: packages + path: _packages/ + + - name: Extract package + run: | + mkdir -p _extracted + ARCHIVE=$(find _packages -name "Radarr.*.linux-core-x64.tar.gz" | head -1) + tar xzf "$ARCHIVE" -C _extracted/ + mkdir -p ./bin/ + cp -r _extracted/Radarr/. ./bin/ 2>/dev/null || cp -r _extracted/. ./bin/ + + - name: Set executable permissions + run: | + chmod a+x _tests/ffprobe + find _tests -name "Radarr.Test.Dummy" -exec chmod a+x {} \; + + - name: Run integration tests + run: | + chmod a+x _tests/test.sh + _tests/test.sh Linux Integration Test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: postgres${{ matrix.postgres-version }}-integration-test-results + path: '**/TestResult.xml' + + # --------------------------------------------------------------------------- + # Windows Installer + # --------------------------------------------------------------------------- + installer: + runs-on: windows-2025 + needs: [build-backend, build-frontend, setup] + env: + RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} + MAJORVERSION: ${{ env.MAJOR_VERSION }} + INNOVERSION: ${{ env.INNO_VERSION }} + BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} + BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download backend output + uses: actions/download-artifact@v4 + with: + name: backend-output + path: _output/ + + - name: Download frontend output + uses: actions/download-artifact@v4 + with: + name: frontend-output + path: _output/UI/ + + - name: Create installer + run: ./build.sh --packages --installer + shell: bash + + - name: Stage installer files + run: | + mkdir -p _installer_output + cp distribution/windows/setup/output/Radarr.*win-x64.exe "_installer_output/Radarr.${BUILDNAME}.windows-core-x64-installer.exe" + cp distribution/windows/setup/output/Radarr.*win-x86.exe "_installer_output/Radarr.${BUILDNAME}.windows-core-x86-installer.exe" + shell: bash + + - name: Upload installer + uses: actions/upload-artifact@v4 + with: + name: windows-installer + path: _installer_output/ + + # --------------------------------------------------------------------------- + # Sentry: upload source maps and create release + # --------------------------------------------------------------------------- + sentry: + runs-on: ubuntu-24.04 + needs: [packages, build-frontend, setup] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_URL: ${{ vars.SENTRY_URL }} + steps: + - name: Download frontend output + uses: actions/download-artifact@v4 + with: + name: frontend-output + path: _output/UI/ + + - name: Install Sentry CLI + run: curl -sL https://sentry.io/get-cli/ | bash + + - name: Upload source maps + env: + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + BRANCH_NAME: ${{ needs.setup.outputs.branch_name }} + run: | + RELEASENAME="Radarr@${RADARR_VERSION}-${BRANCH_NAME}" + sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}" + sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite + sentry-cli releases set-commits --auto "${RELEASENAME}" + if [ "${GITHUB_REF}" = "refs/heads/develop" ]; then + sentry-cli releases deploys "${RELEASENAME}" new -e nightly + else + sentry-cli releases deploys "${RELEASENAME}" new -e production + fi + + # --------------------------------------------------------------------------- + # SonarCloud: Frontend analysis + # --------------------------------------------------------------------------- + sonarcloud-frontend: + runs-on: ubuntu-24.04 + needs: setup + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: SonarCloud scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.projectKey=Radarr_Radarr.UI + -Dsonar.organization=radarr + -Dsonar.sources=./frontend + -Dsonar.host.url=https://sonarcloud.io + -Dsonar.projectVersion=${{ needs.setup.outputs.radarr_version }} + + # --------------------------------------------------------------------------- + # SonarCloud: Backend analysis with coverage + # --------------------------------------------------------------------------- + sonarcloud-backend: + runs-on: windows-2025 + needs: setup + if: needs.setup.outputs.backend_changed == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Enable Windows test service + run: Set-Service SCardSvr -StartupType Manual + shell: pwsh + + - name: Install SonarScanner + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + run: dotnet tool install --global dotnet-sonarscanner + + - name: SonarCloud begin + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet sonarscanner begin \ + /k:"Radarr_Radarr" \ + /o:"radarr" \ + /d:sonar.token="${SONAR_TOKEN}" \ + /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.projectVersion="${{ needs.setup.outputs.radarr_version }}" \ + /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" \ + /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" \ + /d:sonar.cs.cobertura.reportsPaths="$(pwd)/CoverageResults/**/coverage.cobertura.xml" \ + /d:sonar.cs.nunit.reportsPaths="$(pwd)/TestResult.xml" + shell: bash + + - name: Build and run coverage tests + run: | + ./build.sh --backend -f net8.0 -r win-x64 + TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage + shell: bash + + - name: SonarCloud end + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + shell: bash + + - name: Generate coverage report + uses: danielpalme/ReportGenerator-GitHub-Action@5 + if: always() + with: + reports: CoverageResults/**/coverage.cobertura.xml + targetdir: CoverageResults/combined + reporttypes: HtmlInline;Cobertura;Badges + sourcedirs: src + + # --------------------------------------------------------------------------- + # API Docs: generate OpenAPI spec, create PR + # --------------------------------------------------------------------------- + api-docs: + runs-on: windows-2025 + needs: setup + if: github.ref == 'refs/heads/develop' && needs.setup.outputs.backend_changed == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + persist-credentials: true + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Generate OpenAPI spec + run: ./docs.sh Windows + shell: bash + + - name: Commit and create PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.email "ci@starosdev.com" + git config user.name "Starosdev CI" + git checkout -b api-docs + git add . + if git diff --cached --quiet; then + echo "No changes since last run" + else + git commit -m "Automated API Docs update" + git push -f --set-upstream origin api-docs + gh pr create --repo Starosdev/Radarr --title "Update API docs" --base develop --head api-docs --body "Automated API documentation update." || echo "PR already exists" + fi + shell: bash + + - name: Upload API docs + uses: actions/upload-artifact@v4 + if: always() + with: + name: api-docs + path: src/Radarr.Api.V3/openapi.json + + # --------------------------------------------------------------------------- + # Discord Notification + # --------------------------------------------------------------------------- + discord-notify: + runs-on: ubuntu-24.04 + needs: + - setup + - build-backend + - build-frontend + - packages + - lint + - unit-test-native + - unit-test-docker + - unit-test-postgres + - integration-native + - integration-docker + - integration-postgres + - installer + - sentry + - sonarcloud-frontend + - sonarcloud-backend + - api-docs + if: always() && github.event_name == 'push' + steps: + - name: Send Discord notification + env: + DISCORD_WEBHOOK_KEY: ${{ secrets.DISCORD_WEBHOOK_KEY }} + DISCORD_CHANNEL_ID: ${{ vars.DISCORD_CHANNEL_ID }} + BRANCH_NAME: ${{ needs.setup.outputs.branch_name }} + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + run: | + if [ "${{ needs.packages.result }}" = "success" ] && \ + [ "${{ needs.build-backend.result }}" = "success" ] && \ + [ "${{ needs.build-frontend.result }}" = "success" ]; then + COLOR=3066993 + STATUS="Succeeded" + else + COLOR=15158332 + STATUS="Failed" + fi + + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + # Only send if webhook is configured + if [ -n "$DISCORD_WEBHOOK_KEY" ] && [ -n "$DISCORD_CHANNEL_ID" ]; then + curl -s -H "Content-Type: application/json" \ + -d "{ + \"username\": \"GitHub Actions\", + \"embeds\": [{ + \"title\": \"Radarr ${BRANCH_NAME} - ${RADARR_VERSION}\", + \"description\": \"Build ${STATUS}\", + \"url\": \"${RUN_URL}\", + \"color\": ${COLOR}, + \"fields\": [ + {\"name\": \"Branch\", \"value\": \"${BRANCH_NAME}\", \"inline\": true}, + {\"name\": \"Version\", \"value\": \"${RADARR_VERSION}\", \"inline\": true} + ] + }] + }" \ + "https://discord.com/api/webhooks/${DISCORD_CHANNEL_ID}/${DISCORD_WEBHOOK_KEY}" + else + echo "Discord webhook not configured, skipping notification" + fi diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5bb9f297436..6f64ad2a28a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,25 +22,31 @@ variables: linuxImage: 'ubuntu-24.04' macImage: 'macOS-15' -trigger: - branches: - include: - - develop - - master - paths: - exclude: - - .github - - src/Radarr.Api.*/openapi.json +# CI/CD has been migrated to GitHub Actions (.github/workflows/ci.yml) +# Triggers disabled to prevent duplicate builds +trigger: none +pr: none -pr: - branches: - include: - - develop - paths: - exclude: - - .github - - src/NzbDrone.Core/Localization/Core - - src/Radarr.Api.*/openapi.json +# Original trigger configuration (preserved for reference): +# trigger: +# branches: +# include: +# - develop +# - master +# paths: +# exclude: +# - .github +# - src/Radarr.Api.*/openapi.json +# +# pr: +# branches: +# include: +# - develop +# paths: +# exclude: +# - .github +# - src/NzbDrone.Core/Localization/Core +# - src/Radarr.Api.*/openapi.json stages: - stage: Setup From ba7a8a8b1ed805b91e327f0b6972ef87f18d7faf Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 19:51:49 -0400 Subject: [PATCH 02/35] fix(ci): use self-hosted SonarQube instead of SonarCloud (#18) Update sonarcloud-frontend and sonarcloud-backend jobs to use self-hosted SonarQube at sonar.staros.dev with per-project tokens. Remove sonar.organization parameter (not used in SonarQube). --- .github/workflows/ci.yml | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac39f76834f..b29855ad929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -771,9 +771,9 @@ jobs: fi # --------------------------------------------------------------------------- - # SonarCloud: Frontend analysis + # SonarQube: Frontend analysis # --------------------------------------------------------------------------- - sonarcloud-frontend: + sonarqube-frontend: runs-on: ubuntu-24.04 needs: setup if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository @@ -783,22 +783,21 @@ jobs: with: fetch-depth: 0 - - name: SonarCloud scan + - name: SonarQube scan uses: SonarSource/sonarqube-scan-action@v5 env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_FRONTEND }} + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} with: args: > -Dsonar.projectKey=Radarr_Radarr.UI - -Dsonar.organization=radarr -Dsonar.sources=./frontend - -Dsonar.host.url=https://sonarcloud.io -Dsonar.projectVersion=${{ needs.setup.outputs.radarr_version }} # --------------------------------------------------------------------------- - # SonarCloud: Backend analysis with coverage + # SonarQube: Backend analysis with coverage # --------------------------------------------------------------------------- - sonarcloud-backend: + sonarqube-backend: runs-on: windows-2025 needs: setup if: needs.setup.outputs.backend_changed == 'true' @@ -822,16 +821,15 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository run: dotnet tool install --global dotnet-sonarscanner - - name: SonarCloud begin + - name: SonarQube begin if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} run: | dotnet sonarscanner begin \ /k:"Radarr_Radarr" \ - /o:"radarr" \ /d:sonar.token="${SONAR_TOKEN}" \ - /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.host.url="${{ vars.SONAR_HOST_URL }}" \ /d:sonar.projectVersion="${{ needs.setup.outputs.radarr_version }}" \ /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" \ /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" \ @@ -845,11 +843,11 @@ jobs: TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage shell: bash - - name: SonarCloud end + - name: SonarQube end if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} + run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN_BACKEND }}" shell: bash - name: Generate coverage report @@ -928,8 +926,8 @@ jobs: - integration-postgres - installer - sentry - - sonarcloud-frontend - - sonarcloud-backend + - sonarqube-frontend + - sonarqube-backend - api-docs if: always() && github.event_name == 'push' steps: From e0a17c487fe8aeb74d815c071e47b4a6360e3c11 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 19:54:01 -0400 Subject: [PATCH 03/35] fix(ci): separate Discord webhook ID from channel ID (#18) Use DISCORD_WEBHOOK_ID for constructing the webhook URL and DISCORD_CHANNEL_ID for the actual channel reference. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b29855ad929..eca5f7c52d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -934,7 +934,7 @@ jobs: - name: Send Discord notification env: DISCORD_WEBHOOK_KEY: ${{ secrets.DISCORD_WEBHOOK_KEY }} - DISCORD_CHANNEL_ID: ${{ vars.DISCORD_CHANNEL_ID }} + DISCORD_WEBHOOK_ID: ${{ vars.DISCORD_WEBHOOK_ID }} BRANCH_NAME: ${{ needs.setup.outputs.branch_name }} RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} run: | @@ -951,7 +951,7 @@ jobs: RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" # Only send if webhook is configured - if [ -n "$DISCORD_WEBHOOK_KEY" ] && [ -n "$DISCORD_CHANNEL_ID" ]; then + if [ -n "$DISCORD_WEBHOOK_KEY" ] && [ -n "$DISCORD_WEBHOOK_ID" ]; then curl -s -H "Content-Type: application/json" \ -d "{ \"username\": \"GitHub Actions\", @@ -966,7 +966,7 @@ jobs: ] }] }" \ - "https://discord.com/api/webhooks/${DISCORD_CHANNEL_ID}/${DISCORD_WEBHOOK_KEY}" + "https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_KEY}" else echo "Discord webhook not configured, skipping notification" fi From 7a27db3418cf70a92bb885e57e6330a66976ea0f Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 20:03:12 -0400 Subject: [PATCH 04/35] fix(ci): resolve actionlint errors in workflow (#18) Fix env context not available at job-level env (use literal values for MAJORVERSION and INNOVERSION). Pass github.head_ref through env var to prevent script injection risk. --- .github/workflows/ci.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eca5f7c52d4..a579f0c062c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,9 @@ jobs: id: counter env: MAJOR_VERSION: ${{ env.MAJOR_VERSION }} + HEAD_REF: ${{ github.head_ref }} + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} run: | git fetch --tags --force LAST=$(git tag -l 'build-counter-*' | sed 's/build-counter-//' | sort -n | tail -1) @@ -54,10 +57,10 @@ jobs: echo "build_number=$NEXT" >> "$GITHUB_OUTPUT" echo "radarr_version=${MAJOR_VERSION}.${NEXT}" >> "$GITHUB_OUTPUT" - if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "branch_name=${{ github.head_ref }}" >> "$GITHUB_OUTPUT" + if [ "$EVENT_NAME" = "pull_request" ]; then + echo "branch_name=$HEAD_REF" >> "$GITHUB_OUTPUT" else - echo "branch_name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + echo "branch_name=$REF_NAME" >> "$GITHUB_OUTPUT" fi - name: Push build counter tag @@ -695,8 +698,8 @@ jobs: needs: [build-backend, build-frontend, setup] env: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} - MAJORVERSION: ${{ env.MAJOR_VERSION }} - INNOVERSION: ${{ env.INNO_VERSION }} + MAJORVERSION: '6.1.1' + INNOVERSION: '6.2.2' BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} steps: From cd606ada9b602489060cd41719abefadb3dc03fe Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 20:09:21 -0400 Subject: [PATCH 05/35] fix(ci): allow workflow changes to trigger CI (#18) Replace blanket .github/** paths-ignore with specific exclusions for labeler, label-actions, and lock workflows. The CI workflow itself must be able to trigger runs when updated. --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a579f0c062c..72c99854989 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,13 +6,17 @@ on: - develop - master paths-ignore: - - '.github/**' + - '.github/workflows/labeler.yml' + - '.github/workflows/label-actions.yml' + - '.github/workflows/lock.yml' - 'src/Radarr.Api.*/openapi.json' pull_request: branches: - develop paths-ignore: - - '.github/**' + - '.github/workflows/labeler.yml' + - '.github/workflows/label-actions.yml' + - '.github/workflows/lock.yml' - 'src/NzbDrone.Core/Localization/Core/**' - 'src/Radarr.Api.*/openapi.json' From f3953cb8852b75177e148a0a04e215be5df17dde Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 20:19:00 -0400 Subject: [PATCH 06/35] fix(ci): resolve build failures from first CI run (#18) - Fix SA1513 StyleCop error in VideoFileInfoReader.cs (missing blank line after closing brace) - Bump Node.js from 20.11.1 to 20.19.0 (jsdom@28.1.0 requires it) - Fix SonarScanner on Windows: Git Bash mangles /k: and /d: flags, switch to pwsh shell for sonarscanner begin/end steps --- .github/workflows/ci.yml | 30 ++++++++++--------- .../MediaInfo/VideoFileInfoReader.cs | 1 + 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c99854989..45bfb3b3c1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ concurrency: env: MAJOR_VERSION: '6.1.1' DOTNET_VERSION: '8.0.405' - NODE_VERSION: '20.11.1' + NODE_VERSION: '20.19.0' INNO_VERSION: '6.2.2' jobs: @@ -832,17 +832,19 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} - run: | - dotnet sonarscanner begin \ - /k:"Radarr_Radarr" \ - /d:sonar.token="${SONAR_TOKEN}" \ - /d:sonar.host.url="${{ vars.SONAR_HOST_URL }}" \ - /d:sonar.projectVersion="${{ needs.setup.outputs.radarr_version }}" \ - /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" \ - /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" \ - /d:sonar.cs.cobertura.reportsPaths="$(pwd)/CoverageResults/**/coverage.cobertura.xml" \ - /d:sonar.cs.nunit.reportsPaths="$(pwd)/TestResult.xml" - shell: bash + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + run: >- + dotnet sonarscanner begin + /k:"Radarr_Radarr" + /d:sonar.token="$SONAR_TOKEN" + /d:sonar.host.url="$SONAR_HOST_URL" + /d:sonar.projectVersion="$RADARR_VERSION" + /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" + /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" + /d:sonar.cs.cobertura.reportsPaths="${{ github.workspace }}/CoverageResults/**/coverage.cobertura.xml" + /d:sonar.cs.nunit.reportsPaths="${{ github.workspace }}/TestResult.xml" + shell: pwsh - name: Build and run coverage tests run: | @@ -854,8 +856,8 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} - run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN_BACKEND }}" - shell: bash + run: dotnet sonarscanner end /d:sonar.token="$env:SONAR_TOKEN" + shell: pwsh - name: Generate coverage report uses: danielpalme/ReportGenerator-GitHub-Action@5 diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 1bec8e32eec..f6eaadf66d4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -137,6 +137,7 @@ public MediaInfoModel GetMediaInfo(string filename) sideData = sideData.Concat(dvSideData).ToList(); } } + mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData); return mediaInfoModel; From 4efebc239165f83e737a6e3ea1c6314a618ce14b Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 20:38:10 -0400 Subject: [PATCH 07/35] fix(ci): resolve test reporter, webpack, and SonarQube issues (#18) - Remove dorny/test-reporter (requires git checkout, test jobs only download artifacts). Test results still uploaded as artifacts. - Exclude .test.tsx files from webpack build (TS errors from Vitest matchers like toBeInTheDocument) - Fix SonarScanner pwsh env var syntax (use $env:VAR instead of $VAR) --- .github/workflows/ci.yml | 15 +++------------ frontend/build/webpack.config.js | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45bfb3b3c1c..79e64b7904e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -374,15 +374,6 @@ jobs: name: ${{ matrix.test-name }}-unit-test-results path: '**/TestResult.xml' - - name: Report test results - uses: dorny/test-reporter@v1 - if: always() - with: - name: ${{ matrix.test-name }} Unit Tests - path: '**/TestResult.xml' - reporter: java-junit - fail-on-error: true - # --------------------------------------------------------------------------- # Unit Tests: Docker (Alpine / musl) # --------------------------------------------------------------------------- @@ -837,9 +828,9 @@ jobs: run: >- dotnet sonarscanner begin /k:"Radarr_Radarr" - /d:sonar.token="$SONAR_TOKEN" - /d:sonar.host.url="$SONAR_HOST_URL" - /d:sonar.projectVersion="$RADARR_VERSION" + /d:sonar.token="$env:SONAR_TOKEN" + /d:sonar.host.url="$env:SONAR_HOST_URL" + /d:sonar.projectVersion="$env:RADARR_VERSION" /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" /d:sonar.cs.cobertura.reportsPaths="${{ github.workspace }}/CoverageResults/**/coverage.cobertura.xml" diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 6c244c5af3d..ae04b645f3c 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -161,7 +161,7 @@ module.exports = (env) => { rules: [ { test: [/\.jsx?$/, /\.tsx?$/], - exclude: /(node_modules|JsLibraries)/, + exclude: /(node_modules|JsLibraries|\.test\.[jt]sx?$)/, use: [ { loader: 'babel-loader', From ea5db0d3ebb76ebb2d7ab5fca1544baf22573eb9 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 20:44:20 -0400 Subject: [PATCH 08/35] fix: resolve lint errors in frontend test files (#18) - Add explicit boolean values for JSX boolean attributes (ESLint react/jsx-boolean-value rule) - Fix prettier formatting in Icon.test.tsx - Add test infrastructure files to .eslintignore (vitest.config.ts, test/setup.ts, test/cssModuleMock.ts are outside tsconfig scope) --- frontend/.eslintignore | 3 +++ frontend/src/Components/Icon.test.tsx | 8 ++------ frontend/src/Components/Label.test.tsx | 2 +- frontend/src/Components/ProgressBar.test.tsx | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/frontend/.eslintignore b/frontend/.eslintignore index e6d49ec4d77..0a9891fcf7e 100644 --- a/frontend/.eslintignore +++ b/frontend/.eslintignore @@ -1,2 +1,5 @@ **/JsLibraries/** **/*.css.d.ts +**/test/setup.ts +**/test/cssModuleMock.ts +vitest.config.ts diff --git a/frontend/src/Components/Icon.test.tsx b/frontend/src/Components/Icon.test.tsx index 61f79ca41d8..8e14a7b444a 100644 --- a/frontend/src/Components/Icon.test.tsx +++ b/frontend/src/Components/Icon.test.tsx @@ -50,11 +50,7 @@ describe('Icon', () => { it('applies containerClassName to the title wrapper span', () => { render( - + ); const span = screen.getByTitle('Test'); @@ -69,7 +65,7 @@ describe('Icon', () => { }); it('renders spinning icon when isSpinning is true', () => { - const { container } = render(); + const { container } = render(); const svg = container.querySelector('svg') as SVGElement; expect(svg.classList.toString()).toContain('spin'); diff --git a/frontend/src/Components/Label.test.tsx b/frontend/src/Components/Label.test.tsx index 254c08289b0..66894a15292 100644 --- a/frontend/src/Components/Label.test.tsx +++ b/frontend/src/Components/Label.test.tsx @@ -31,7 +31,7 @@ describe('Label', () => { }); it('applies outline class when outline is true', () => { - render(); + render(); const el = screen.getByText('Outline'); expect(el.className).toContain('outline'); diff --git a/frontend/src/Components/ProgressBar.test.tsx b/frontend/src/Components/ProgressBar.test.tsx index 5319b942a03..d4ae271484a 100644 --- a/frontend/src/Components/ProgressBar.test.tsx +++ b/frontend/src/Components/ProgressBar.test.tsx @@ -29,13 +29,13 @@ describe('ProgressBar', () => { }); it('shows text when showText is true', () => { - render(); + render(); expect(screen.getByText('50.0%')).toBeInTheDocument(); }); it('shows custom text when provided', () => { - render(); + render(); expect(screen.getByText('Half done')).toBeInTheDocument(); }); From dda5e10ac88404ea122c8828ff77edc37d6b2487 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 21:08:16 -0400 Subject: [PATCH 09/35] fix(ci): exclude test files from webpack/tsc, fix SonarScanner args (#18) - Exclude *.test.ts/tsx from tsconfig.json so fork-ts-checker-webpack-plugin does not type-check Vitest test files during production build - Fix SonarScanner begin command: use pwsh backtick continuations instead of YAML folding, use /v: for project version per scanner recommendation --- .github/workflows/ci.yml | 20 ++++++++++---------- frontend/tsconfig.json | 4 +++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e64b7904e..7945e99946a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -825,16 +825,16 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} - run: >- - dotnet sonarscanner begin - /k:"Radarr_Radarr" - /d:sonar.token="$env:SONAR_TOKEN" - /d:sonar.host.url="$env:SONAR_HOST_URL" - /d:sonar.projectVersion="$env:RADARR_VERSION" - /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" - /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" - /d:sonar.cs.cobertura.reportsPaths="${{ github.workspace }}/CoverageResults/**/coverage.cobertura.xml" - /d:sonar.cs.nunit.reportsPaths="${{ github.workspace }}/TestResult.xml" + run: | + dotnet sonarscanner begin ` + /k:"Radarr_Radarr" ` + /d:sonar.token="$env:SONAR_TOKEN" ` + /d:sonar.host.url="$env:SONAR_HOST_URL" ` + /v:"$env:RADARR_VERSION" ` + /d:sonar.exclusions="**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**" ` + /d:sonar.coverage.exclusions="**/Radarr.Api.V3/**/*" ` + /d:sonar.cs.cobertura.reportsPaths="${{ github.workspace }}/CoverageResults/**/coverage.cobertura.xml" ` + /d:sonar.cs.nunit.reportsPaths="${{ github.workspace }}/TestResult.xml" shell: pwsh - name: Build and run coverage tests diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 611c872edaa..4cd39f3fcf2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -32,6 +32,8 @@ "./typings/*.ts", ], "exclude": [ - "node_modules" + "node_modules", + "src/**/*.test.ts", + "src/**/*.test.tsx" ] } From 1fc11bad7d84cd7625c28c6f7d59f98ba0008edb Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 21:28:13 -0400 Subject: [PATCH 10/35] fix(ci): handle missing net8.0-windows output, exclude tests from ESLint (#18) - Make PackageWindows() gracefully skip the net8.0-windows overlay when building on Linux (WinForms tray app only builds on Windows) - Add *.test.ts and *.test.tsx to .eslintignore (test files use Vitest types not included in the main tsconfig project) --- build.sh | 6 +++++- frontend/.eslintignore | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 73e785bebe0..6a9f14a2a52 100755 --- a/build.sh +++ b/build.sh @@ -206,7 +206,11 @@ PackageWindows() local folder=$artifactsFolder/$runtime/$framework/Radarr PackageFiles "$folder" "$framework" "$runtime" - cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder + if [ -d "$outputFolder/$framework-windows/$runtime/publish" ]; then + cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder + else + echo "Skipping Windows-specific overlay (net8.0-windows not built on this platform)" + fi echo "Removing Radarr.Mono" rm -f $folder/Radarr.Mono.* diff --git a/frontend/.eslintignore b/frontend/.eslintignore index 0a9891fcf7e..e66f7f45c07 100644 --- a/frontend/.eslintignore +++ b/frontend/.eslintignore @@ -1,5 +1,7 @@ **/JsLibraries/** **/*.css.d.ts +**/*.test.ts +**/*.test.tsx **/test/setup.ts **/test/cssModuleMock.ts vitest.config.ts From 1d8f15011851bf59fa99d9f95271eb53b2d1f8f6 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 21:52:53 -0400 Subject: [PATCH 11/35] fix(ci): fix installer and sentry source map upload (#18) - Installer job now builds backend on Windows directly (needs net8.0-windows TFM for Radarr.exe WinForms tray app, which only builds on Windows) - Update sentry-cli source map command from deprecated 'releases files upload-sourcemaps' to 'sourcemaps upload' --- .github/workflows/ci.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7945e99946a..c456219cfb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -690,7 +690,7 @@ jobs: # --------------------------------------------------------------------------- installer: runs-on: windows-2025 - needs: [build-backend, build-frontend, setup] + needs: [build-frontend, setup] env: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} MAJORVERSION: '6.1.1' @@ -701,13 +701,17 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: + submodules: true fetch-depth: 1 - - name: Download backend output - uses: actions/download-artifact@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - name: backend-output - path: _output/ + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Build backend on Windows + run: ./build.sh --backend + shell: bash - name: Download frontend output uses: actions/download-artifact@v4 @@ -760,7 +764,7 @@ jobs: run: | RELEASENAME="Radarr@${RADARR_VERSION}-${BRANCH_NAME}" sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}" - sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite + sentry-cli sourcemaps upload --release="${RELEASENAME}" -p radarr-ui _output/UI/ sentry-cli releases set-commits --auto "${RELEASENAME}" if [ "${GITHUB_REF}" = "refs/heads/develop" ]; then sentry-cli releases deploys "${RELEASENAME}" new -e nightly From 46322c43f0948356da7ea2242070f7bb72abe765 Mon Sep 17 00:00:00 2001 From: Eder Date: Wed, 18 Mar 2026 22:17:00 -0400 Subject: [PATCH 12/35] fix(ci): add checkout to sentry job for set-commits (#18) sentry-cli releases set-commits --auto needs a git repo to associate commits with the release. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c456219cfb3..634e2f3a745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -748,6 +748,11 @@ jobs: SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_URL: ${{ vars.SENTRY_URL }} steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download frontend output uses: actions/download-artifact@v4 with: From 290f806fa072b0b4b7c85527670218df518647ee Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 12:18:04 -0400 Subject: [PATCH 13/35] feat(ci): update Sentry config for Starosdev fork (#21) Replace upstream Servarr Sentry org and URL with Starosdev's sentry.io instance. Backend and frontend DSNs were already updated; this covers the remaining CI pipeline references in azure-pipelines.yml. --- azure-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6f64ad2a28a..74b38a6fe27 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,8 +13,8 @@ variables: minorVersion: $[counter('minorVersion', 2000)] radarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)' - sentryOrg: 'servarr' - sentryUrl: 'https://sentry.servarr.com' + sentryOrg: 'staros-labs' + sentryUrl: 'https://sentry.io' dotnetVersion: '8.0.405' nodeVersion: '20.X' innoVersion: '6.2.2' @@ -383,7 +383,7 @@ stages: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) ) env: - SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr) + SENTRY_AUTH_TOKEN: $(sentryAuthToken) SENTRY_ORG: $(sentryOrg) SENTRY_URL: $(sentryUrl) From ac7a380279ba486c461d264ffe0da45086312c2c Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 12:25:05 -0400 Subject: [PATCH 14/35] fix(host): use ordinal comparison for Uri pattern matching (#22) Replace CurrentCultureIgnoreCase and InvariantCultureIgnoreCase with OrdinalIgnoreCase in HTTP middleware and request extensions to prevent startup crashes under Turkish (tr-TR) locale. Also replace ToLower() with ToLowerInvariant() in InitializeJsonController. --- src/Radarr.Http/Extensions/RequestExtensions.cs | 2 +- src/Radarr.Http/Frontend/InitializeJsonController.cs | 2 +- src/Radarr.Http/Middleware/CacheableSpecification.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Radarr.Http/Extensions/RequestExtensions.cs b/src/Radarr.Http/Extensions/RequestExtensions.cs index 57260b012ac..e1203bcfe23 100644 --- a/src/Radarr.Http/Extensions/RequestExtensions.cs +++ b/src/Radarr.Http/Extensions/RequestExtensions.cs @@ -36,7 +36,7 @@ public static class RequestExtensions public static bool IsApiRequest(this HttpRequest request) { - return request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase); + return request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase); } public static bool GetBooleanQueryParameter(this HttpRequest request, string parameter, bool defaultValue = false) diff --git a/src/Radarr.Http/Frontend/InitializeJsonController.cs b/src/Radarr.Http/Frontend/InitializeJsonController.cs index 3089fa6ac90..11f7ecaa436 100644 --- a/src/Radarr.Http/Frontend/InitializeJsonController.cs +++ b/src/Radarr.Http/Frontend/InitializeJsonController.cs @@ -51,7 +51,7 @@ private string GetContent() builder.AppendLine($" \"version\": \"{BuildInfo.Version.ToString()}\","); builder.AppendLine($" \"instanceName\": \"{_configFileProvider.InstanceName.ToString()}\","); builder.AppendLine($" \"theme\": \"{_configFileProvider.Theme.ToString()}\","); - builder.AppendLine($" \"branch\": \"{_configFileProvider.Branch.ToLower()}\","); + builder.AppendLine($" \"branch\": \"{_configFileProvider.Branch.ToLowerInvariant()}\","); builder.AppendLine($" \"analytics\": {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); builder.AppendLine($" \"userHash\": \"{HashUtil.AnonymousToken()}\","); builder.AppendLine($" \"urlBase\": \"{_urlBase}\","); diff --git a/src/Radarr.Http/Middleware/CacheableSpecification.cs b/src/Radarr.Http/Middleware/CacheableSpecification.cs index 23e9709d8a3..255bea7cc93 100644 --- a/src/Radarr.Http/Middleware/CacheableSpecification.cs +++ b/src/Radarr.Http/Middleware/CacheableSpecification.cs @@ -24,7 +24,7 @@ public bool IsCacheable(HttpRequest request) return true; } - if (request.Path.StartsWithSegments("/api", StringComparison.CurrentCultureIgnoreCase)) + if (request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) { if (request.Path.ToString().ContainsIgnoreCase("/MediaCover")) { @@ -34,7 +34,7 @@ public bool IsCacheable(HttpRequest request) return false; } - if (request.Path.StartsWithSegments("/signalr", StringComparison.CurrentCultureIgnoreCase)) + if (request.Path.StartsWithSegments("/signalr", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -51,14 +51,14 @@ public bool IsCacheable(HttpRequest request) return false; } - if (path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) + if (path.StartsWith("/feed", StringComparison.OrdinalIgnoreCase)) { return false; } - if ((path.StartsWith("/logfile", StringComparison.CurrentCultureIgnoreCase) || - path.StartsWith("/updatelogfile", StringComparison.CurrentCultureIgnoreCase)) && - path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) + if ((path.StartsWith("/logfile", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/updatelogfile", StringComparison.OrdinalIgnoreCase)) && + path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) { return false; } From fa06a5b70f964f1120797249791e1deac37f281b Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 12:34:46 -0400 Subject: [PATCH 15/35] fix(host): use ordinal comparison for remaining EndsWith calls (#22) --- src/Radarr.Http/Middleware/CacheableSpecification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Radarr.Http/Middleware/CacheableSpecification.cs b/src/Radarr.Http/Middleware/CacheableSpecification.cs index 255bea7cc93..41a668341a5 100644 --- a/src/Radarr.Http/Middleware/CacheableSpecification.cs +++ b/src/Radarr.Http/Middleware/CacheableSpecification.cs @@ -41,12 +41,12 @@ public bool IsCacheable(HttpRequest request) var path = request.Path.Value ?? ""; - if (path.EndsWith("/index.js")) + if (path.EndsWith("/index.js", StringComparison.Ordinal)) { return false; } - if (path.EndsWith("/initialize.json")) + if (path.EndsWith("/initialize.json", StringComparison.Ordinal)) { return false; } From 001635eb9dd5e86bf44a1c3b6106719e2a4a1ca1 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 12:51:31 -0400 Subject: [PATCH 16/35] fix(http): serve intermediate CA certificates from PFX on Linux (#23) Load the full certificate chain from PFX files using X509Certificate2Collection instead of a single X509Certificate2. The leaf certificate and intermediate CAs are separated, then passed to SslStreamCertificateContext.Create() so Kestrel serves the complete chain during TLS handshake. Previously, only the leaf certificate was loaded, which caused SSL validation failures on Linux/Docker where the OS certificate store does not automatically resolve intermediates from the PFX. --- src/NzbDrone.Host/Bootstrap.cs | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 1d9309748ba..f627ef2c30d 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data.SQLite; using System.IO; +using System.Net.Security; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -190,9 +191,16 @@ public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContex { if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace()) { + var (certificate, extraChain) = LoadSslCertificateWithChain(sslCertPath, sslCertPassword); + var certContext = SslStreamCertificateContext.Create(certificate, extraChain); + options.ConfigureHttpsDefaults(configureOptions => { - configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword); + configureOptions.ServerCertificate = certificate; + configureOptions.OnAuthenticate = (_, sslOptions) => + { + sslOptions.ServerCertificateContext = certContext; + }; }); } }); @@ -272,13 +280,32 @@ private static string BuildUrl(string scheme, string bindAddress, int port) return $"{scheme}://{bindAddress}:{port}"; } - private static X509Certificate2 ValidateSslCertificate(string cert, string password) + private static (X509Certificate2 Certificate, X509Certificate2Collection ExtraChain) LoadSslCertificateWithChain(string cert, string password) { - X509Certificate2 certificate; + X509Certificate2 leafCertificate = null; + var extraChain = new X509Certificate2Collection(); try { - certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet); + var collection = new X509Certificate2Collection(); + collection.Import(cert, password, X509KeyStorageFlags.DefaultKeySet); + + foreach (var certificate in collection) + { + if (certificate.HasPrivateKey) + { + leafCertificate = certificate; + } + else + { + extraChain.Add(certificate); + } + } + + if (leafCertificate == null) + { + throw new RadarrStartupException($"The SSL certificate file {cert} does not contain a certificate with a private key"); + } } catch (CryptographicException ex) { @@ -291,7 +318,7 @@ private static X509Certificate2 ValidateSslCertificate(string cert, string passw throw new RadarrStartupException(ex); } - return certificate; + return (leafCertificate, extraChain); } } } From ae5b47fb639bc7227f6a2c7f88c71a1fbce38dee Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 13:16:57 -0400 Subject: [PATCH 17/35] fix(ci): update Inno Setup to 6.4.2 and add curl error handling The installer job failed because the jrsoftware.org download returned an HTML error page instead of the binary. The curl command silently saved the error page as innosetup.exe. This adds --fail and -L flags to curl so HTTP errors are caught, and bumps the version from 6.2.2 to 6.4.2. --- .github/workflows/ci.yml | 4 ++-- build.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 634e2f3a745..c7118876fe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ env: MAJOR_VERSION: '6.1.1' DOTNET_VERSION: '8.0.405' NODE_VERSION: '20.19.0' - INNO_VERSION: '6.2.2' + INNO_VERSION: '6.4.2' jobs: # --------------------------------------------------------------------------- @@ -694,7 +694,7 @@ jobs: env: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} MAJORVERSION: '6.1.1' - INNOVERSION: '6.2.2' + INNOVERSION: '6.4.2' BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} steps: diff --git a/build.sh b/build.sh index 6a9f14a2a52..bbdd38406c6 100755 --- a/build.sh +++ b/build.sh @@ -258,7 +258,7 @@ InstallInno() ProgressStart "Installing portable Inno Setup" rm -rf _inno - curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe" + curl -s -L --fail --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.4.2}.exe" mkdir _inno ./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno rm innosetup.exe From 414207b0ef1468c19311487f1c930bdf401a2154 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 13:45:53 -0400 Subject: [PATCH 18/35] fix(ci): use Chocolatey for Inno Setup, fix Sentry set-commits The jrsoftware.org file server is unreliable, causing the installer job to fail when downloading innosetup.exe. Replaced the direct download with Chocolatey installation on Windows CI runners. Also added --ignore-missing to sentry-cli set-commits to handle cases where the previous release SHA is not in the git history. --- .github/workflows/ci.yml | 8 +++++++- build.sh | 24 ++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7118876fe9..44c7da7d9be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -719,9 +719,15 @@ jobs: name: frontend-output path: _output/UI/ + - name: Install Inno Setup + run: choco install innosetup --version=${{ env.INNO_VERSION }} -y --no-progress + shell: pwsh + - name: Create installer run: ./build.sh --packages --installer shell: bash + env: + ISCC_PATH: 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' - name: Stage installer files run: | @@ -770,7 +776,7 @@ jobs: RELEASENAME="Radarr@${RADARR_VERSION}-${BRANCH_NAME}" sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}" sentry-cli sourcemaps upload --release="${RELEASENAME}" -p radarr-ui _output/UI/ - sentry-cli releases set-commits --auto "${RELEASENAME}" + sentry-cli releases set-commits --auto --ignore-missing "${RELEASENAME}" if [ "${GITHUB_REF}" = "refs/heads/develop" ]; then sentry-cli releases deploys "${RELEASENAME}" new -e nightly else diff --git a/build.sh b/build.sh index bbdd38406c6..c38b8952a6d 100755 --- a/build.sh +++ b/build.sh @@ -249,25 +249,41 @@ BuildInstaller() { local framework="$1" local runtime="$2" - - ./_inno/ISCC.exe distribution/windows/setup/radarr.iss "//DFramework=$framework" "//DRuntime=$runtime" + local iscc + + if [ -n "${ISCC_PATH:-}" ] && [ -f "${ISCC_PATH}" ]; then + iscc="${ISCC_PATH}" + else + iscc="./_inno/ISCC.exe" + fi + + "${iscc}" distribution/windows/setup/radarr.iss "//DFramework=$framework" "//DRuntime=$runtime" } InstallInno() { + if [ -n "${ISCC_PATH:-}" ] && [ -f "${ISCC_PATH}" ]; then + echo "Using system Inno Setup at ${ISCC_PATH}" + return + fi + ProgressStart "Installing portable Inno Setup" - + rm -rf _inno curl -s -L --fail --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.4.2}.exe" mkdir _inno ./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno rm innosetup.exe - + ProgressEnd "Installed portable Inno Setup" } RemoveInno() { + if [ -n "${ISCC_PATH:-}" ]; then + return + fi + rm -rf _inno } From f3a17f6cca07d34f04b3948e4128d8893ed2a875 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 14:30:37 -0400 Subject: [PATCH 19/35] fix(ci): use pre-installed Inno Setup on Windows runner Inno Setup 6.7.1 is already available on windows-2025 runners. Instead of downloading or installing via Chocolatey, detect the existing ISCC.exe path dynamically and pass it to build.sh via ISCC_PATH. --- .github/workflows/ci.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44c7da7d9be..336a7cd6f76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -719,15 +719,22 @@ jobs: name: frontend-output path: _output/UI/ - - name: Install Inno Setup - run: choco install innosetup --version=${{ env.INNO_VERSION }} -y --no-progress + - name: Find Inno Setup + id: find-inno + run: | + $iscc = Get-ChildItem -Path "C:\Program Files*\Inno Setup*" -Filter "ISCC.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($iscc) { + echo "ISCC_PATH=$($iscc.FullName)" >> $env:GITHUB_ENV + Write-Host "Found ISCC at: $($iscc.FullName)" + } else { + Write-Host "::error::Inno Setup not found on runner" + exit 1 + } shell: pwsh - name: Create installer run: ./build.sh --packages --installer shell: bash - env: - ISCC_PATH: 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' - name: Stage installer files run: | From d6769a71dfdcf3fd5e8aaf7258e37beaa900fe2c Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:25:48 -0400 Subject: [PATCH 20/35] feat(docker): add Dockerfile for container builds --- .dockerignore | 2 ++ Dockerfile | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..6f3c9a7cabf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +* +!radarr/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..4800d55dd05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble + +COPY --chmod=755 radarr /opt/radarr + +RUN groupadd -g 1000 radarr && \ + useradd -u 1000 -g radarr -d /config -s /bin/bash radarr && \ + mkdir -p /config && chown radarr:radarr /config + +ENV RADARR_BRANCH="develop" \ + XDG_CONFIG_HOME="/config/xdg" + +VOLUME /config +EXPOSE 7878 + +USER radarr +ENTRYPOINT ["/opt/radarr/Radarr", "-nobrowser", "-data=/config"] From 825a463cccfd011a37d506699f6c235c07021976 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:27:30 -0400 Subject: [PATCH 21/35] feat(ci): add Gitleaks secret scanning workflow --- .github/workflows/secret-scan.yml | 56 +++++++++++++++++++++++++++++++ .gitleaks.toml | 12 +++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/secret-scan.yml create mode 100644 .gitleaks.toml diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml new file mode 100644 index 00000000000..709edd9f63a --- /dev/null +++ b/.github/workflows/secret-scan.yml @@ -0,0 +1,56 @@ +name: Secret Scan + +on: + push: + branches: [develop, master] + pull_request: + branches: [develop, master] + workflow_dispatch: + +concurrency: + group: "secret-scan-${{ github.ref }}" + cancel-in-progress: true + +permissions: + contents: read + security-events: write + +jobs: + gitleaks: + name: Gitleaks Secret Detection + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run gitleaks scan + run: | + docker run --rm \ + -v "$PWD:/repo" \ + zricethezav/gitleaks:v8.30.0 \ + detect \ + --source=/repo \ + --no-git \ + --config=/repo/.gitleaks.toml \ + --report-format=sarif \ + --report-path=/repo/gitleaks-results.sarif \ + --redact + + - name: Upload gitleaks report + if: always() + uses: actions/upload-artifact@v4 + with: + name: gitleaks-results + path: gitleaks-results.sarif + retention-days: 30 + if-no-files-found: warn + + - name: Upload SARIF to GitHub Security + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: gitleaks-results.sarif + continue-on-error: true diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000000..b5d13cfaf2d --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,12 @@ +[global] +# Radarr-specific gitleaks config + +[allowlist] +description = "Global allowlist" +paths = [ + '''\.github/workflows/''', + '''node_modules/''', + '''_output/''', + '''_tests/''', + '''_artifacts/''', +] From b8f04cdd589051dc5e946f53fa6df56b12156f54 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:27:34 -0400 Subject: [PATCH 22/35] feat(ci): add GitHub Release job for develop and master builds --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 336a7cd6f76..55d8e57b958 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -282,6 +282,83 @@ jobs: name: packages path: _archives/ + # --------------------------------------------------------------------------- + # Release: create GitHub Release with package assets + # --------------------------------------------------------------------------- + release: + runs-on: ubuntu-24.04 + needs: [packages, installer, setup] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + permissions: + contents: write + env: + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + BRANCH_NAME: ${{ needs.setup.outputs.branch_name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: packages + path: _release/ + + - name: Download installer + uses: actions/download-artifact@v4 + with: + name: windows-installer + path: _release/ + + - name: Generate checksums + working-directory: _release + run: | + sha256sum * > sha256sums.txt + cat sha256sums.txt + + - name: Determine release type + id: release-type + run: | + if [ "${{ github.ref }}" = "refs/heads/master" ]; then + echo "prerelease=false" >> "$GITHUB_OUTPUT" + echo "tag=v${RADARR_VERSION}" >> "$GITHUB_OUTPUT" + else + echo "prerelease=true" >> "$GITHUB_OUTPUT" + echo "tag=v${RADARR_VERSION}-nightly" >> "$GITHUB_OUTPUT" + fi + + - name: Create git tag + env: + TAG: ${{ steps.release-type.outputs.tag }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${TAG}" -m "Release ${TAG}" + git push origin "${TAG}" + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release-type.outputs.tag }} + PRERELEASE: ${{ steps.release-type.outputs.prerelease }} + run: | + RELEASE_ARGS=( + "${TAG}" + --repo Starosdev/Radarr + --title "Radarr v${RADARR_VERSION}" + --generate-notes + ) + + if [ "${PRERELEASE}" = "true" ]; then + RELEASE_ARGS+=(--prerelease) + else + RELEASE_ARGS+=(--latest) + fi + + gh release create "${RELEASE_ARGS[@]}" _release/* + # --------------------------------------------------------------------------- # Lint # --------------------------------------------------------------------------- From 620ad0751d617c3508e7d4be5307db42a54c8625 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:27:34 -0400 Subject: [PATCH 23/35] feat(ci): add stale branch cleanup workflow --- .github/workflows/cleanup-branches.yml | 138 +++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .github/workflows/cleanup-branches.yml diff --git a/.github/workflows/cleanup-branches.yml b/.github/workflows/cleanup-branches.yml new file mode 100644 index 00000000000..eca6a66c6e3 --- /dev/null +++ b/.github/workflows/cleanup-branches.yml @@ -0,0 +1,138 @@ +name: Cleanup Stale Branches + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 3 AM UTC + workflow_dispatch: + +permissions: + contents: write + +jobs: + cleanup: + name: Delete Stale Merged Branches + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name "GitHub Actions Bot" + git config --global user.email "actions@github.com" + + - name: Fetch All Branches + run: git fetch --all --prune + + - name: Find and Delete Stale Branches + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "==================================================" + echo "Stale Branch Cleanup - $(date)" + echo "==================================================" + echo "" + + PROTECTED_BRANCHES="master develop main" + DELETED_COUNT=0 + KEPT_COUNT=0 + + echo "Scanning for stale branches (merged >30 days ago)..." + echo "" + + for branch in $(git branch -r | grep -v '\->' | grep -v 'HEAD' | sed 's/origin\///'); do + skip=false + for protected in $PROTECTED_BRANCHES; do + if [ "$branch" = "$protected" ]; then + skip=true + break + fi + done + + if [ "$skip" = true ]; then + continue + fi + + merged_to_develop=$(git branch -r --merged origin/develop | grep -c "origin/$branch" || echo "0") + merged_to_master=$(git branch -r --merged origin/master | grep -c "origin/$branch" || echo "0") + + # Check GitHub API for squash-merged PRs + if [ "$merged_to_develop" = "0" ] && [ "$merged_to_master" = "0" ]; then + pr_merged=$(gh pr list --repo "${{ github.repository }}" --head "$branch" --state merged --json number --jq 'length' 2>/dev/null || echo "0") + if [ "$pr_merged" != "0" ] && [ "$pr_merged" != "" ]; then + pr_base=$(gh pr list --repo "${{ github.repository }}" --head "$branch" --state merged --json baseRefName --jq '.[0].baseRefName' 2>/dev/null || echo "") + if [ "$pr_base" = "develop" ]; then + merged_to_develop="1" + elif [ "$pr_base" = "master" ]; then + merged_to_master="1" + else + merged_to_develop="1" + fi + fi + fi + + if [ "$merged_to_develop" = "0" ] && [ "$merged_to_master" = "0" ]; then + KEPT_COUNT=$((KEPT_COUNT + 1)) + continue + fi + + LAST_COMMIT_DATE=$(git log -1 --format="%ci" "origin/$branch" 2>/dev/null || echo "1970-01-01") + LAST_COMMIT_EPOCH=$(date -d "$LAST_COMMIT_DATE" +%s 2>/dev/null || echo "0") + CURRENT_EPOCH=$(date +%s) + DAYS_OLD=$(( (CURRENT_EPOCH - LAST_COMMIT_EPOCH) / 86400 )) + + if [ "$DAYS_OLD" -gt 30 ]; then + merged_to="" + [ "$merged_to_develop" != "0" ] && merged_to="develop" + if [ "$merged_to_master" != "0" ]; then + [ -n "$merged_to" ] && merged_to="$merged_to and master" || merged_to="master" + fi + + echo " Deleting: $branch" + echo " Last commit: $DAYS_OLD days ago" + echo " Merged to: $merged_to" + + git push origin --delete "$branch" 2>&1 | sed 's/^/ /' || { + echo " WARNING: Failed to delete $branch" + } + + DELETED_COUNT=$((DELETED_COUNT + 1)) + echo "" + else + KEPT_COUNT=$((KEPT_COUNT + 1)) + fi + done + + echo "" + echo "==================================================" + echo "Cleanup Summary" + echo "==================================================" + echo "Deleted branches: $DELETED_COUNT" + echo "Kept branches: $KEPT_COUNT" + echo "Protected branches: $PROTECTED_BRANCHES" + + - name: List Remaining Feature Branches + run: | + echo "" + echo "==================================================" + echo "Remaining Feature/Development Branches" + echo "==================================================" + + PATTERN='feature/|fix/|hotfix/' + REMAINING=$(git branch -r | grep -E "$PATTERN" | grep -v 'origin/master' | grep -v 'origin/develop' | sed 's/origin\///' | wc -l) + + if [ "$REMAINING" -gt 0 ]; then + echo "Found $REMAINING active feature branches:" + echo "" + git branch -r | grep -E "$PATTERN" | grep -v 'origin/master' | grep -v 'origin/develop' | sed 's/origin\///' | while read branch; do + LAST_COMMIT=$(git log -1 --format="%ci" "origin/$branch" 2>/dev/null | cut -d' ' -f1) + AUTHOR=$(git log -1 --format="%an" "origin/$branch" 2>/dev/null) + echo " $branch" + echo " Last commit: $LAST_COMMIT by $AUTHOR" + done + else + echo "No active feature branches remaining." + fi From e84321261747d3da64e1bf93d9384e6f10be717f Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:27:59 -0400 Subject: [PATCH 24/35] feat(ci): add Docker build and push to GHCR --- .github/workflows/ci.yml | 110 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55d8e57b958..058c0450291 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -359,6 +359,116 @@ jobs: gh release create "${RELEASE_ARGS[@]}" _release/* + # --------------------------------------------------------------------------- + # Docker: build and push per-arch images to GHCR + # --------------------------------------------------------------------------- + docker: + runs-on: ubuntu-24.04 + needs: [packages, setup] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + permissions: + contents: read + packages: write + env: + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} + REGISTRY: ghcr.io + IMAGE_NAME: starosdev/radarr + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + archive_rid: linux-core-x64 + - platform: linux/arm64 + archive_rid: linux-core-arm64 + - platform: linux/arm/v7 + archive_rid: linux-core-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: packages + path: _archives/ + + - name: Extract package for platform + run: | + mkdir -p radarr + tar xzf _archives/Radarr.${BUILDNAME}.${{ matrix.archive_rid }}.tar.gz -C radarr/ + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.platform }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: ${{ matrix.platform }} + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.RADARR_VERSION }}-${{ matrix.archive_rid }} + labels: | + org.opencontainers.image.title=Radarr + org.opencontainers.image.version=${{ env.RADARR_VERSION }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + cache-from: type=gha,scope=${{ matrix.archive_rid }} + cache-to: type=gha,mode=max,scope=${{ matrix.archive_rid }} + + # --------------------------------------------------------------------------- + # Docker Manifest: combine per-arch images into multi-arch manifest + # --------------------------------------------------------------------------- + docker-manifest: + runs-on: ubuntu-24.04 + needs: [docker, setup] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + permissions: + packages: write + env: + RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} + REGISTRY: ghcr.io + IMAGE_NAME: starosdev/radarr + steps: + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifests + run: | + VERSION_TAG="${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}" + BRANCH_TAG="${REGISTRY}/${IMAGE_NAME}:${{ github.ref == 'refs/heads/master' && 'latest' || 'develop' }}" + + docker manifest create "${VERSION_TAG}" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm" + docker manifest push "${VERSION_TAG}" + + docker manifest create "${BRANCH_TAG}" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" \ + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm" + docker manifest push "${BRANCH_TAG}" + # --------------------------------------------------------------------------- # Lint # --------------------------------------------------------------------------- From 3c1fb1af0adf2215a0407c74ca7a1c77bdba6625 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:28:36 -0400 Subject: [PATCH 25/35] feat(ci): update Discord notification, clean up Inno Setup env vars --- .github/workflows/ci.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 058c0450291..0e1f1f48f3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ env: MAJOR_VERSION: '6.1.1' DOTNET_VERSION: '8.0.405' NODE_VERSION: '20.19.0' - INNO_VERSION: '6.4.2' jobs: # --------------------------------------------------------------------------- @@ -881,7 +880,6 @@ jobs: env: RADARRVERSION: ${{ needs.setup.outputs.radarr_version }} MAJORVERSION: '6.1.1' - INNOVERSION: '6.4.2' BUILD_SOURCEBRANCHNAME: ${{ needs.setup.outputs.branch_name }} BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} steps: @@ -1134,6 +1132,8 @@ jobs: - integration-docker - integration-postgres - installer + - release + - docker-manifest - sentry - sonarqube-frontend - sonarqube-backend @@ -1159,6 +1159,18 @@ jobs: RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + RELEASE_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/v${RADARR_VERSION}${{ github.ref == 'refs/heads/develop' && '-nightly' || '' }}" + + RELEASE_FIELD="" + if [ "${{ needs.release.result }}" = "success" ]; then + RELEASE_FIELD=",{\"name\": \"Release\", \"value\": \"[v${RADARR_VERSION}](${RELEASE_URL})\", \"inline\": true}" + fi + + DOCKER_FIELD="" + if [ "${{ needs.docker-manifest.result }}" = "success" ]; then + DOCKER_FIELD=",{\"name\": \"Docker\", \"value\": \"ghcr.io/starosdev/radarr:${RADARR_VERSION}\", \"inline\": true}" + fi + # Only send if webhook is configured if [ -n "$DISCORD_WEBHOOK_KEY" ] && [ -n "$DISCORD_WEBHOOK_ID" ]; then curl -s -H "Content-Type: application/json" \ @@ -1171,7 +1183,7 @@ jobs: \"color\": ${COLOR}, \"fields\": [ {\"name\": \"Branch\", \"value\": \"${BRANCH_NAME}\", \"inline\": true}, - {\"name\": \"Version\", \"value\": \"${RADARR_VERSION}\", \"inline\": true} + {\"name\": \"Version\", \"value\": \"${RADARR_VERSION}\", \"inline\": true}${RELEASE_FIELD}${DOCKER_FIELD} ] }] }" \ From 4bcef63fd3277f5cbb9e90291745a2e63b2b3167 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 20:29:20 -0400 Subject: [PATCH 26/35] chore: add docs/plans/ to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0c080c21b8d..7bd0d863013 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,4 @@ node_modules.nosync # Claude Code .claude/ CLAUDE.md +docs/plans/ From e1be92a930e3473274f5241aa93bd9b5cdf0b610 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 21:10:03 -0400 Subject: [PATCH 27/35] fix(docker): handle existing GID/UID in base image --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4800d55dd05..4e87c9a4a78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,9 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble COPY --chmod=755 radarr /opt/radarr -RUN groupadd -g 1000 radarr && \ - useradd -u 1000 -g radarr -d /config -s /bin/bash radarr && \ - mkdir -p /config && chown radarr:radarr /config +RUN groupadd -f -g 1000 radarr && \ + useradd -u 1000 -g 1000 -d /config -s /bin/bash radarr 2>/dev/null; \ + mkdir -p /config && chown 1000:1000 /config ENV RADARR_BRANCH="develop" \ XDG_CONFIG_HOME="/config/xdg" From efd743b328c95b6c0dd76c00db1ee48dd7487d17 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 21:36:57 -0400 Subject: [PATCH 28/35] fix(ci): drop arm/v7 from Docker builds (no Noble base image) --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e1f1f48f3a..0224ad547b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -381,8 +381,6 @@ jobs: archive_rid: linux-core-x64 - platform: linux/arm64 archive_rid: linux-core-arm64 - - platform: linux/arm/v7 - archive_rid: linux-core-arm steps: - name: Checkout uses: actions/checkout@v4 @@ -458,14 +456,12 @@ jobs: docker manifest create "${VERSION_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ - "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" \ - "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm" + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" docker manifest push "${VERSION_TAG}" docker manifest create "${BRANCH_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ - "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" \ - "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm" + "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" docker manifest push "${BRANCH_TAG}" # --------------------------------------------------------------------------- From 90770946ce1bd35d2d63fb4b1f6adf779360a857 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 22:00:52 -0400 Subject: [PATCH 29/35] fix(ci): add --amend flag to docker manifest create --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0224ad547b8..7fae65f826a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -454,12 +454,12 @@ jobs: VERSION_TAG="${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}" BRANCH_TAG="${REGISTRY}/${IMAGE_NAME}:${{ github.ref == 'refs/heads/master' && 'latest' || 'develop' }}" - docker manifest create "${VERSION_TAG}" \ + docker manifest create --amend "${VERSION_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" docker manifest push "${VERSION_TAG}" - docker manifest create "${BRANCH_TAG}" \ + docker manifest create --amend "${BRANCH_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" docker manifest push "${BRANCH_TAG}" From 3ae09934b0241b0e78496c7920f4e4f1f9ea57ff Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 21 Mar 2026 22:27:06 -0400 Subject: [PATCH 30/35] fix(ci): use buildx imagetools for multi-arch manifest creation --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fae65f826a..e471f351904 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -449,20 +449,21 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Create and push manifests run: | VERSION_TAG="${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}" BRANCH_TAG="${REGISTRY}/${IMAGE_NAME}:${{ github.ref == 'refs/heads/master' && 'latest' || 'develop' }}" - docker manifest create --amend "${VERSION_TAG}" \ + docker buildx imagetools create -t "${VERSION_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" - docker manifest push "${VERSION_TAG}" - docker manifest create --amend "${BRANCH_TAG}" \ + docker buildx imagetools create -t "${BRANCH_TAG}" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-x64" \ "${REGISTRY}/${IMAGE_NAME}:${RADARR_VERSION}-linux-core-arm64" - docker manifest push "${BRANCH_TAG}" # --------------------------------------------------------------------------- # Lint From 2591eb75cb8c4fad49a9ed33a24fcb02f29b2544 Mon Sep 17 00:00:00 2001 From: Eder <34795193+Starosdev@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:50:18 -0400 Subject: [PATCH 31/35] fix(ci): update GHCR image namespace to staros-labs --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e471f351904..8d8798e432e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -372,7 +372,7 @@ jobs: RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} BUILDNAME: ${{ needs.setup.outputs.branch_name }}.${{ needs.setup.outputs.radarr_version }} REGISTRY: ghcr.io - IMAGE_NAME: starosdev/radarr + IMAGE_NAME: staros-labs/radarr strategy: fail-fast: false matrix: @@ -440,7 +440,7 @@ jobs: env: RADARR_VERSION: ${{ needs.setup.outputs.radarr_version }} REGISTRY: ghcr.io - IMAGE_NAME: starosdev/radarr + IMAGE_NAME: staros-labs/radarr steps: - name: Login to GHCR uses: docker/login-action@v3 From 0fd0785ae4dee65e42c67d64be9bae4968c59a42 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 9 May 2026 18:40:50 -0400 Subject: [PATCH 32/35] =?UTF-8?q?[OPS]=20v1.0.0=20=E2=80=94=20standardize?= =?UTF-8?q?=20develop=20and=20master=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cleanup-branches.yml | 2 +- AGENTS.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/.github/workflows/cleanup-branches.yml b/.github/workflows/cleanup-branches.yml index eca6a66c6e3..f6c1dd06934 100644 --- a/.github/workflows/cleanup-branches.yml +++ b/.github/workflows/cleanup-branches.yml @@ -36,7 +36,7 @@ jobs: echo "==================================================" echo "" - PROTECTED_BRANCHES="master develop main" + PROTECTED_BRANCHES="master develop" DELETED_COUNT=0 KEPT_COUNT=0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..704956c93f8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# Repository Guidelines + +## Working Rules + +- Work from a git worktree when making repo changes. +- Do not use emojis in commits, pull requests, comments, or docs. +- Do not mention assistant product names in commits, pull requests, comments, or repo files. Use role-based terms such as `builder` or `reviewer` if needed. +- This fork uses `develop` as the integration branch and `master` as the production branch. + +## Branch Guidance + +- Land ongoing fork work on `develop`. +- Treat `master` as the production branch. +- Keep workflow and release guidance aligned with that branch model. From 9407929aeef962766384c04d18c68504a18dd5e4 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 9 May 2026 19:09:14 -0400 Subject: [PATCH 33/35] =?UTF-8?q?[OPS]=20v1.0.1=20=E2=80=94=20bump=20MailK?= =?UTF-8?q?it=20to=20patched=20release=20for=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NzbDrone.Core/Radarr.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Radarr.Core.csproj b/src/NzbDrone.Core/Radarr.Core.csproj index da90842bf21..104832c7075 100644 --- a/src/NzbDrone.Core/Radarr.Core.csproj +++ b/src/NzbDrone.Core/Radarr.Core.csproj @@ -6,7 +6,7 @@ - + From ccdf8d07a3a9628aed27826b38fac78e3894a4a6 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 9 May 2026 19:35:48 -0400 Subject: [PATCH 34/35] fix(ci): patch MailKit vuln and harden secret scan MailKit 4.13.0 has GHSA-9j88-vvj5-vhgr; bump to 4.16.0 to unblock CI. Branch cleanup grep used substring match; switch to -cxF to prevent false deletion of unmerged branches. Secret scan lacked full history and skipped workflow files; drop --no-git, fetch full depth, and remove .github/workflows/ from gitleaks allowlist. --- .github/workflows/cleanup-branches.yml | 4 ++-- .github/workflows/secret-scan.yml | 3 +-- .gitleaks.toml | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cleanup-branches.yml b/.github/workflows/cleanup-branches.yml index f6c1dd06934..8d28e2a2ba2 100644 --- a/.github/workflows/cleanup-branches.yml +++ b/.github/workflows/cleanup-branches.yml @@ -56,8 +56,8 @@ jobs: continue fi - merged_to_develop=$(git branch -r --merged origin/develop | grep -c "origin/$branch" || echo "0") - merged_to_master=$(git branch -r --merged origin/master | grep -c "origin/$branch" || echo "0") + merged_to_develop=$(git branch -r --merged origin/develop | grep -cxF " origin/$branch" || echo "0") + merged_to_master=$(git branch -r --merged origin/master | grep -cxF " origin/$branch" || echo "0") # Check GitHub API for squash-merged PRs if [ "$merged_to_develop" = "0" ] && [ "$merged_to_master" = "0" ]; then diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 709edd9f63a..02240552f42 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -24,7 +24,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 1 + fetch-depth: 0 - name: Run gitleaks scan run: | @@ -33,7 +33,6 @@ jobs: zricethezav/gitleaks:v8.30.0 \ detect \ --source=/repo \ - --no-git \ --config=/repo/.gitleaks.toml \ --report-format=sarif \ --report-path=/repo/gitleaks-results.sarif \ diff --git a/.gitleaks.toml b/.gitleaks.toml index b5d13cfaf2d..1004d3ee545 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -4,7 +4,6 @@ [allowlist] description = "Global allowlist" paths = [ - '''\.github/workflows/''', '''node_modules/''', '''_output/''', '''_tests/''', From 565a764fc3b2e62c3c3a6ad9c789a4f529aba271 Mon Sep 17 00:00:00 2001 From: Eder Date: Sat, 9 May 2026 20:22:31 -0400 Subject: [PATCH 35/35] fix(ci): make release tag creation idempotent Tag push fails with exit 128 when the nightly tag already exists from a prior run. Delete local and remote tag before recreating so re-runs on the same version do not abort. --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d8798e432e..ffce4faad6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -334,6 +334,8 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -d "${TAG}" 2>/dev/null || true + git push origin ":refs/tags/${TAG}" 2>/dev/null || true git tag -a "${TAG}" -m "Release ${TAG}" git push origin "${TAG}"