diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0938d3f5b..ef0b36a17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,13 +39,14 @@ jobs: with: persist-credentials: false - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash @@ -118,14 +119,15 @@ jobs: CI: true run: pnpm install - - name: Lint - run: pnpm lint + - parallel: + - name: Lint + run: pnpm lint - - name: Formatting - run: pnpm format:check + - name: Formatting + run: pnpm format:check - - name: Build - run: pnpm build + - name: Build + run: pnpm build unit-test: name: Unit Tests @@ -186,18 +188,19 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - - name: Warm cache (no push) — ${{ matrix.platform }} uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -222,17 +225,14 @@ jobs: packages: write id-token: write steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Log in to Docker Hub uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 @@ -247,6 +247,10 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + - name: Extract metadata id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6187f0588..99ccaf45d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,16 +36,17 @@ jobs: matrix: language: [actions, javascript] steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Initialize CodeQL - uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality + - name: Initialize CodeQL + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 35c7787fd..19e13ff9d 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -22,16 +22,17 @@ jobs: outputs: tag_version: ${{ steps.git-cliff.outputs.tag_version }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install git-cliff - uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 - with: - tool: git-cliff + - name: Install git-cliff + uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 + with: + tool: git-cliff - name: Get tag version id: git-cliff @@ -59,21 +60,22 @@ jobs: with: ssh-key: '${{ secrets.COMMIT_KEY }}' - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' - # For workflows with elevated privileges we recommend disabling automatic caching. - # https://github.com/actions/setup-node - package-manager-cache: false + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: 'package.json' + # For workflows with elevated privileges we recommend disabling automatic caching. + # https://github.com/actions/setup-node + package-manager-cache: false - - name: Configure git - run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" + - name: Configure git + run: | + git config --global user.name "${{ github.actor }}" + git config --global user.email "${{ github.actor }}@users.noreply.github.com" - name: Bump package.json run: npm version ${TAG_VERSION} --no-commit-hooks --no-git-tag-version diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 5bd7a12ac..50ff3d63c 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -40,15 +40,15 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false - - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index 35b0f98a6..502c5531d 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -25,13 +25,14 @@ jobs: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index a308422d0..565ebf839 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -28,15 +28,15 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false - - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Get pnpm store directory shell: sh diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 7cc3550bd..78ab02045 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -27,24 +27,25 @@ jobs: outputs: has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 + - name: Install helm + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - name: Install Oras - uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 + - name: Install Oras + uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 - - name: Login to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Package helm charts run: | @@ -92,33 +93,34 @@ jobs: needs: [package-helm-chart] if: needs.package-helm-chart.outputs.has_artifacts == 'true' steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 + - name: Install helm + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - name: Install Oras - uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 + - name: Install Oras + uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 - - name: Install Cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - name: Downloads artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: artifacts - path: .cr-release-packages/ + - name: Downloads artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: artifacts + path: .cr-release-packages/ - - name: Login to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Push charts to GHCR env: @@ -150,27 +152,28 @@ jobs: permissions: contents: read steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - name: Install Cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - name: Downloads artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: artifacts - path: .cr-release-packages/ + - name: Downloads artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: artifacts + path: .cr-release-packages/ - - name: Login to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Verify signatures for each chart tag run: | diff --git a/.github/workflows/lint-helm-charts.yml b/.github/workflows/lint-helm-charts.yml index 5f4e6c113..9e1853e5d 100644 --- a/.github/workflows/lint-helm-charts.yml +++ b/.github/workflows/lint-helm-charts.yml @@ -27,29 +27,31 @@ jobs: permissions: contents: read steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 + - name: Set up Helm + uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 - - name: Set up chart-testing - uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 - - name: Ensure documentation is updated - uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c + - parallel: + - name: Ensure documentation is updated + uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c - - name: Run chart-testing (list-changed) - id: list-changed - run: | - changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) - if [[ -n "$changed" ]]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "$changed" - fi + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) + if [[ -n "$changed" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "$changed" + fi - name: Run chart-testing if: steps.list-changed.outputs.changed == 'true' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 8b76f9db6..e87e89c5c 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -31,82 +31,83 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - if: always() && steps.lint_pr_title.outputs.error_message != null - env: - ERROR_MESSAGE: ${{ steps.lint_pr_title.outputs.error_message }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const message = process.env.ERROR_MESSAGE; - const prNumber = context.payload.pull_request.number; + - parallel: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + if: always() && steps.lint_pr_title.outputs.error_message != null + env: + ERROR_MESSAGE: ${{ steps.lint_pr_title.outputs.error_message }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const message = process.env.ERROR_MESSAGE; + const prNumber = context.payload.pull_request.number; - const body = [ - `### PR Title Validation Failed\n`, - message, - `\n---\n`, - `PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).`, - `*This check will re-run when you update your PR title.*`, - ].join('\n'); + const body = [ + `### PR Title Validation Failed\n`, + message, + `\n---\n`, + `PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).`, + `*This check will re-run when you update your PR title.*`, + ].join('\n'); - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') + ); - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); - } + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - if: always() && steps.lint_pr_title.outputs.error_message == null - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + if: always() && steps.lint_pr_title.outputs.error_message == null + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### PR Title Validation Failed') + ); - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - }); - } + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } template-check: name: Validate PR Template @@ -117,10 +118,16 @@ jobs: issues: write pull-requests: write steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Skip bot PRs + id: bot-check + if: github.event.pull_request.user.type == 'Bot' + run: echo "skip=true" >> "$GITHUB_OUTPUT" + + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -128,11 +135,6 @@ jobs: node-version-file: 'package.json' package-manager-cache: false - - name: Skip bot PRs - id: bot-check - if: github.event.pull_request.user.type == 'Bot' - run: echo "skip=true" >> "$GITHUB_OUTPUT" - - name: Write PR body to file if: steps.bot-check.outputs.skip != 'true' env: @@ -156,130 +158,131 @@ jobs: } >> "$GITHUB_OUTPUT" exit 0 - - name: Label and comment on failure - if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code != '0' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - ISSUES_JSON: ${{ steps.check.outputs.issues }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issues = JSON.parse(process.env.ISSUES_JSON); - const author = process.env.PR_AUTHOR; - const prNumber = context.payload.pull_request.number; - const LABEL = 'blocked:template'; + - parallel: + - name: Label and comment on failure + if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code != '0' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + ISSUES_JSON: ${{ steps.check.outputs.issues }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issues = JSON.parse(process.env.ISSUES_JSON); + const author = process.env.PR_AUTHOR; + const prNumber = context.payload.pull_request.number; + const LABEL = 'blocked:template'; - const issueList = issues.map(i => `- ${i}`).join('\n'); + const issueList = issues.map(i => `- ${i}`).join('\n'); - const commentBody = [ - `Hey @${author}, thanks for submitting this PR! However, it looks like the PR template hasn't been fully filled out.\n`, - `### Issues found:\n`, - issueList, - `\n---\n`, - `**Please update your PR description to follow the [PR template](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/.github/PULL_REQUEST_TEMPLATE.md).**`, - `Incomplete or missing PR descriptions may indicate insufficient review of the changes, and PRs that do not follow the template **may be closed without review**.`, - `See our [Contributing Guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/CONTRIBUTING.md) for more details.\n`, - `*This check will automatically re-run when you edit your PR description.*`, - ].join('\n'); + const commentBody = [ + `Hey @${author}, thanks for submitting this PR! However, it looks like the PR template hasn't been fully filled out.\n`, + `### Issues found:\n`, + issueList, + `\n---\n`, + `**Please update your PR description to follow the [PR template](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/.github/PULL_REQUEST_TEMPLATE.md).**`, + `Incomplete or missing PR descriptions may indicate insufficient review of the changes, and PRs that do not follow the template **may be closed without review**.`, + `See our [Contributing Guide](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/develop/CONTRIBUTING.md) for more details.\n`, + `*This check will automatically re-run when you edit your PR description.*`, + ].join('\n'); - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') + ); - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: commentBody, - }); - } + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody, + }); + } - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: [LABEL], - }); - } catch (e) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: LABEL, - color: 'B60205', - description: 'PR template not properly filled out', - }); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: [LABEL], - }); - } catch (e2) { - console.log('Could not create/add label:', e2.message); - } - } + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [LABEL], + }); + } catch (e) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL, + color: 'B60205', + description: 'PR template not properly filled out', + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [LABEL], + }); + } catch (e2) { + console.log('Could not create/add label:', e2.message); + } + } - core.setFailed('PR template is not properly filled out.'); + core.setFailed('PR template is not properly filled out.'); - - name: Remove label on success - if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code == '0' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const prNumber = context.payload.pull_request.number; - const LABEL = 'blocked:template'; + - name: Remove label on success + if: steps.bot-check.outputs.skip != 'true' && steps.check.outputs.exit_code == '0' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = context.payload.pull_request.number; + const LABEL = 'blocked:template'; - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - name: LABEL, - }); - } catch (e) { - console.log('Could not remove label', e.message); - } + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: LABEL, + }); + } catch (e) { + console.log('Could not remove label', e.message); + } - const allComments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - } - ); + const allComments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = allComments.find( - c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') - ); + const botComment = allComments.find( + c => c.user.type === 'Bot' && c.body && c.body.includes('### Issues found:') + ); - if (botComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - }); - } + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index fb52c968d..b6cbba581 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -32,28 +32,29 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + - name: Derive preview version from tag + id: ver + shell: bash + run: | + TAG="${GITHUB_REF_NAME}" + VER="${TAG#preview-}" + VER="${VER#v}" + echo "version=${VER}" >> "$GITHUB_OUTPUT" + echo "Building preview version: ${VER}" - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - - - name: Derive preview version from tag - id: ver - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - VER="${TAG#preview-}" - VER="${VER#v}" - echo "version=${VER}" >> "$GITHUB_OUTPUT" - echo "Building preview version: ${VER}" - - name: Warm cache (no push) — ${{ matrix.platform }} uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -78,17 +79,24 @@ jobs: packages: write id-token: write steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + - name: Derive preview version from tag + id: ver + shell: bash + run: | + TAG="${GITHUB_REF_NAME}" + VER="${TAG#preview-}" + VER="${VER#v}" + echo "version=${VER}" >> "$GITHUB_OUTPUT" + echo "Publishing preview version: ${VER}" - name: Log in to Docker Hub uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 @@ -102,16 +110,9 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Derive preview version from tag - id: ver - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - VER="${TAG#preview-}" - VER="${VER#v}" - echo "version=${VER}" >> "$GITHUB_OUTPUT" - echo "Publishing preview version: ${VER}" + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - name: Extract metadata id: meta diff --git a/.github/workflows/rebuild-issue-index.yml b/.github/workflows/rebuild-issue-index.yml index 764b8dd22..92fceb1aa 100644 --- a/.github/workflows/rebuild-issue-index.yml +++ b/.github/workflows/rebuild-issue-index.yml @@ -3,7 +3,7 @@ name: Rebuild Issue Index on: schedule: - - cron: "0 3 * * *" + - cron: '0 3 * * *' workflow_dispatch: permissions: {} @@ -23,13 +23,14 @@ jobs: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - parallel: + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: 'package.json' + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: 'package.json' - name: Get pnpm store directory shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38b6cecff..943d542d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,18 +75,19 @@ jobs: env: VERSION: ${{ github.ref_name }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - - name: Warm cache [${{ matrix.platform }}] uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -114,17 +115,14 @@ jobs: env: VERSION: ${{ github.ref_name }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Commit timestamp - id: ts - run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Log in to Docker Hub uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 @@ -139,6 +137,10 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Commit timestamp + id: ts + run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" + - name: Extract metadata id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 @@ -172,23 +174,24 @@ jobs: cache-to: type=gha,mode=max provenance: false - - name: Resolve manifest digest - id: digests - run: | - DIGEST=$(docker buildx imagetools inspect "${{ env.DOCKER_HUB }}:${{ env.VERSION }}" --format '{{json .Manifest.Digest}}' | tr -d '"') - echo "IMAGE_DIGEST=$DIGEST" >> $GITHUB_OUTPUT + - parallel: + - name: Resolve manifest digest + id: digests + run: | + DIGEST=$(docker buildx imagetools inspect "${{ env.DOCKER_HUB }}:${{ env.VERSION }}" --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "IMAGE_DIGEST=$DIGEST" >> $GITHUB_OUTPUT - - name: Also tag :latest (non-pre-release only) - shell: bash - if: ${{ !contains(env.VERSION, '-') }} - run: | - docker buildx imagetools create \ - -t ${{ env.DOCKER_HUB }}:latest \ - ${{ env.DOCKER_HUB }}:${{ env.VERSION }} + - name: Also tag :latest (non-pre-release only) + shell: bash + if: ${{ !contains(env.VERSION, '-') }} + run: | + docker buildx imagetools create \ + -t ${{ env.DOCKER_HUB }}:latest \ + ${{ env.DOCKER_HUB }}:${{ env.VERSION }} - docker buildx imagetools create \ - -t ghcr.io/${{ github.repository }}:latest \ - ghcr.io/${{ github.repository }}:${{ env.VERSION }} + docker buildx imagetools create \ + -t ghcr.io/${{ github.repository }}:latest \ + ghcr.io/${{ github.repository }}:${{ env.VERSION }} sign: name: Sign images and create SBOM attestations @@ -202,16 +205,17 @@ jobs: VERSION: ${{ github.ref_name }} COSIGN_YES: 'true' steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false + - parallel: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - - name: Install Cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - name: Install Trivy - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 + - name: Install Trivy + uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 - name: Log in to Docker Hub uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 @@ -271,27 +275,28 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - name: Verify signatures - run: | - cosign verify "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - --certificate-oidc-issuer "https://token.actions.githubusercontent.com" + - parallel: + - name: Verify signatures + run: | + cosign verify "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ + --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" - cosign verify "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ - --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - --certificate-oidc-issuer "https://token.actions.githubusercontent.com" + cosign verify "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ + --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" - # - name: Verify attestations - # run: | - # cosign verify-attestation "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ - # --type cyclonedx \ - # --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - # --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null + - name: Verify attestations + run: | + cosign verify-attestation "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \ + --type cyclonedx \ + --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" - # cosign verify-attestation "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ - # --type cyclonedx \ - # --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ - # --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null + cosign verify-attestation "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \ + --type cyclonedx \ + --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" publish-release: name: Publish release diff --git a/.github/workflows/renovate-helm-custom-hooks.yml b/.github/workflows/renovate-helm-custom-hooks.yml index 0ecfbb760..7c91fe521 100644 --- a/.github/workflows/renovate-helm-custom-hooks.yml +++ b/.github/workflows/renovate-helm-custom-hooks.yml @@ -24,20 +24,21 @@ jobs: pull-requests: write if: github.actor == 'renovate[bot]' steps: - - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false + - parallel: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false - - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - id: app-token - with: - app-id: 2138788 - private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }} + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + client-id: ${{ secrets.APP_SEERR_HELM_CLIENT_ID }} + private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }} - - name: Set up chart-testing - uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + - name: Set up chart-testing + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 - name: Run chart-testing (list-changed) id: list-changed diff --git a/.github/workflows/seerr-labeller.yml b/.github/workflows/seerr-labeller.yml index 7a8330555..823bb8bd6 100644 --- a/.github/workflows/seerr-labeller.yml +++ b/.github/workflows/seerr-labeller.yml @@ -27,39 +27,40 @@ jobs: NUMBER: ${{ github.event.pull_request.number }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} steps: - - name: Label added, comment and close pull request - if: github.event.action == 'labeled' && github.event.label.name == 'ai-generated' - shell: bash - env: - BODY: > - :wave: @${{ env.PR_AUTHOR }}, thank you for your contribution! + - parallel: + - name: Label added, comment and close pull request + if: github.event.action == 'labeled' && github.event.label.name == 'ai-generated' + shell: bash + env: + BODY: > + :wave: @${{ env.PR_AUTHOR }}, thank you for your contribution! - However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision. + However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision. - AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. + AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight. - This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. - If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look. - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true - retry gh pr close "$NUMBER" -R "$GH_REPO" || true - gh pr lock "$NUMBER" -R "$GH_REPO" -r "spam" || true + This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. + If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look. + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true + retry gh pr close "$NUMBER" -R "$GH_REPO" || true + gh pr lock "$NUMBER" -R "$GH_REPO" -r "spam" || true - - name: Label removed, reopen and unlock pull request - if: github.event.action == 'unlabeled' && github.event.label.name == 'ai-generated' - shell: bash - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh pr reopen "$NUMBER" -R "$GH_REPO" || true - gh pr unlock "$NUMBER" -R "$GH_REPO" || true + - name: Label removed, reopen and unlock pull request + if: github.event.action == 'unlabeled' && github.event.label.name == 'ai-generated' + shell: bash + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh pr reopen "$NUMBER" -R "$GH_REPO" || true + gh pr unlock "$NUMBER" -R "$GH_REPO" || true - - name: Remove AI-generated label on manual reopen - if: github.event.action == 'reopened' - shell: bash - run: | - gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "ai-generated" || true - gh pr unlock "$NUMBER" -R "$GH_REPO" || true + - name: Remove AI-generated label on manual reopen + if: github.event.action == 'reopened' + shell: bash + run: | + gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "ai-generated" || true + gh pr unlock "$NUMBER" -R "$GH_REPO" || true support: if: > @@ -78,34 +79,35 @@ jobs: NUMBER: ${{ github.event.issue.number }} ISSUE_AUTHOR: ${{ github.event.issue.user.login }} steps: - - name: Label added, comment and close issue - if: github.event.action == 'labeled' && github.event.label.name == 'support' - shell: bash - env: - BODY: > - :wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively - for bug reports and feature requests. However, this issue appears - to be a support request. Please use our support channels - to get help with Seerr. + - parallel: + - name: Label added, comment and close issue + if: github.event.action == 'labeled' && github.event.label.name == 'support' + shell: bash + env: + BODY: > + :wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively + for bug reports and feature requests. However, this issue appears + to be a support request. Please use our support channels + to get help with Seerr. - - [Discord](https://discord.gg/seerr) - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true - retry gh issue close "$NUMBER" -R "$GH_REPO" || true - gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true + - [Discord](https://discord.gg/seerr) + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true + retry gh issue close "$NUMBER" -R "$GH_REPO" || true + gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true - - name: Label removed, reopen and unlock issue - if: github.event.action == 'unlabeled' && github.event.label.name == 'support' - shell: bash - run: | - retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } - retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true - gh issue unlock "$NUMBER" -R "$GH_REPO" || true + - name: Label removed, reopen and unlock issue + if: github.event.action == 'unlabeled' && github.event.label.name == 'support' + shell: bash + run: | + retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; } + retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true + gh issue unlock "$NUMBER" -R "$GH_REPO" || true - - name: Remove support label on manual reopen - if: github.event.action == 'reopened' - shell: bash - run: | - gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true - gh issue unlock "$NUMBER" -R "$GH_REPO" || true + - name: Remove support label on manual reopen + if: github.event.action == 'reopened' + shell: bash + run: | + gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true + gh issue unlock "$NUMBER" -R "$GH_REPO" || true diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index d4e01ee04..bc70576c5 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -29,14 +29,15 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - package-manager-cache: false + - parallel: + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + package-manager-cache: false - - name: Pnpm Setup - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - name: Pnpm Setup + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Get pnpm store directory shell: sh diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 7b46f5ac3..be55ae8f0 100644 --- a/charts/seerr-chart/Chart.yaml +++ b/charts/seerr-chart/Chart.yaml @@ -3,9 +3,9 @@ kubeVersion: '>=1.23.0-0' name: seerr-chart description: Seerr helm chart for Kubernetes type: application -version: 3.7.0 +version: 3.9.0 # renovate: image=ghcr.io/seerr-team/seerr -appVersion: 'v3.3.0' +appVersion: 'v3.4.0' maintainers: - name: Seerr Team url: https://github.com/orgs/seerr-team/people diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index 7c509770d..f624c2387 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -1,6 +1,6 @@ # seerr-chart -![Version: 3.7.0](https://img.shields.io/badge/Version-3.7.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.3.0](https://img.shields.io/badge/AppVersion-v3.3.0-informational?style=flat-square) +![Version: 3.9.0](https://img.shields.io/badge/Version-3.9.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.4.0](https://img.shields.io/badge/AppVersion-v3.4.0-informational?style=flat-square) Seerr helm chart for Kubernetes @@ -53,9 +53,12 @@ If `replicaCount` value was used - remove it. Helm update should work fine after | config.persistence.storageClass | string | `""` | Storage class for the PVC. Set to "-" to disable dynamic provisioning. Uses default storage class if no value is provided | | config.persistence.subPath | string | `""` | Subpath of the pvc which should be mounted | | config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. | +| dnsConfig | object | `{}` | docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config | +| dnsPolicy | string | `""` | docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy | | extraEnv | list | `[]` | Environment variables to add to the seerr pods | | extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the seerr pods | | fullnameOverride | string | `""` | | +| hostUsers | bool | `true` | docs: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ | | image.pullPolicy | string | `"IfNotPresent"` | | | image.registry | string | `"ghcr.io"` | | | image.repository | string | `"seerr-team/seerr"` | | @@ -75,6 +78,7 @@ If `replicaCount` value was used - remove it. Helm update should work fine after | podLabels | object | `{}` | | | podSecurityContext.fsGroup | int | `1000` | | | podSecurityContext.fsGroupChangePolicy | string | `"OnRootMismatch"` | | +| priorityClassName | string | `nil` | Specify a priorityclass, or use default if unset. | | probes.livenessProbe | object | `{"initialDelaySeconds":20,"periodSeconds":15,"timeoutSeconds":3}` | Configure liveness probe | | probes.readinessProbe | object | `{"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":3}` | Configure readiness probe | | probes.startupProbe | string | `nil` | Configure startup probe | diff --git a/charts/seerr-chart/templates/statefulset.yaml b/charts/seerr-chart/templates/statefulset.yaml index 1888ea419..1ce545fff 100644 --- a/charts/seerr-chart/templates/statefulset.yaml +++ b/charts/seerr-chart/templates/statefulset.yaml @@ -28,6 +28,19 @@ spec: serviceAccountName: {{ include "seerr.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- if kindIs "bool" .Values.hostUsers }} + hostUsers: {{ .Values.hostUsers }} + {{- end }} + {{- with .Values.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with .Values.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: diff --git a/charts/seerr-chart/values.yaml b/charts/seerr-chart/values.yaml index 5321f44a7..adc8a9765 100644 --- a/charts/seerr-chart/values.yaml +++ b/charts/seerr-chart/values.yaml @@ -67,6 +67,31 @@ securityContext: seccompProfile: type: RuntimeDefault +# -- Set a DNS policy, likely not needed unless seerr only interacts with external services. +# -- docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy +dnsPolicy: '' + +# -- Set DNS config options +# -- Setting ndots lower can prevent queries like `raw.githubusercontent.com.seerr.svc.cluster.local.` +# -- docs: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config +dnsConfig: {} +# nameservers: +# - 1.2.3.4 +# searches: +# - ns1.svc.cluster-domain.example +# - my.dns.search.suffix +# options: +# - name: ndots +# value: "2" + +# -- Set to false to run in an idmaped user namespace for additional isolation +# -- requires cluster support, and PV support of idmaped volumes. +# -- docs: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ +hostUsers: true + +# -- Specify a priorityclass, or use default if unset. +priorityClassName: null + service: type: ClusterIP port: 80 diff --git a/devenv.nix b/devenv.nix index 39e48287a..7c1c502af 100644 --- a/devenv.nix +++ b/devenv.nix @@ -8,6 +8,7 @@ packages = [ pkgs.prettier pkgs.husky + pkgs.helm-docs ]; languages.javascript = { diff --git a/server/api/servarr/radarr.test.ts b/server/api/servarr/radarr.test.ts new file mode 100644 index 000000000..f3aaed384 --- /dev/null +++ b/server/api/servarr/radarr.test.ts @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it, mock } from 'node:test'; + +import type { AxiosInstance } from 'axios'; + +import RadarrAPI from '@server/api/servarr/radarr'; + +function buildRadarr(): RadarrAPI { + return new RadarrAPI({ url: 'http://localhost:7878/api/v3', apiKey: 'test' }); +} + +function getAxios(radarr: RadarrAPI): AxiosInstance { + return (radarr as unknown as { axios: AxiosInstance }).axios; +} + +describe('RadarrAPI removeMovie', () => { + afterEach(() => mock.restoreAll()); + + it('removes the movie when it exists in the library', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await radarr.removeMovie(550); + + assert.strictEqual(del.mock.callCount(), 1); + assert.strictEqual(del.mock.calls[0].arguments[0], '/movie/7'); + }); + + it('does nothing when the movie is not in the library', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ + data: [{ id: 0, title: 'Fight Club' }], + })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.doesNotReject(() => radarr.removeMovie(550)); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('rejects when the tmdbId is unknown to the lookup', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ data: [] })); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.rejects(() => radarr.removeMovie(550), /Movie not found/); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('ignores a 404 when the movie was already removed in Radarr', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + mock.method(getAxios(radarr), 'delete', async () => { + throw { response: { status: 404 } }; + }); + + await assert.doesNotReject(() => radarr.removeMovie(550)); + }); + + it('rethrows errors other than 404', async () => { + const radarr = buildRadarr(); + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + })); + mock.method(getAxios(radarr), 'delete', async () => { + throw { response: { status: 500 } }; + }); + + await assert.rejects(() => radarr.removeMovie(550)); + }); + + it('rethrows a 404 from the lookup instead of treating it as removed', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => { + throw { response: { status: 404 } }; + }); + const del = mock.method(getAxios(radarr), 'delete', async () => ({})); + + await assert.rejects( + () => radarr.removeMovie(550), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 404 + ); + assert.strictEqual(del.mock.callCount(), 0); + }); +}); + +describe('RadarrAPI getMovieByTmdbId', () => { + afterEach(() => mock.restoreAll()); + + it('rethrows a 401 from the lookup with the status intact', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => { + throw { response: { status: 401 } }; + }); + + await assert.rejects( + () => radarr.getMovieByTmdbId(550), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 401 + ); + }); + + it('throws "Movie not found" when the lookup returns no results', async () => { + const radarr = buildRadarr(); + mock.method(getAxios(radarr), 'get', async () => ({ data: [] })); + + await assert.rejects(() => radarr.getMovieByTmdbId(550), { + message: 'Movie not found', + }); + }); +}); diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 1d35778c0..4a30dfdad 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -1,4 +1,5 @@ import logger from '@server/logger'; +import type { AxiosResponse } from 'axios'; import ServarrBase from './base'; export interface RadarrMovieOptions { @@ -93,26 +94,27 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { }; public async getMovieByTmdbId(id: number): Promise { + let response: AxiosResponse; try { - const response = await this.axios.get('/movie/lookup', { + response = await this.axios.get('/movie/lookup', { params: { term: `tmdb:${id}`, }, }); - - if (!response.data[0]) { - throw new Error('Movie not found'); - } - - return response.data[0]; } catch (e) { logger.error('Error retrieving movie by TMDB ID', { label: 'Radarr API', errorMessage: e.message, tmdbId: id, }); - throw new Error('Movie not found', { cause: e }); + throw e; } + + if (!response.data[0]) { + throw new Error('Movie not found'); + } + + return response.data[0]; } public addMovie = async ( @@ -267,9 +269,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { ); } } - public removeMovie = async (movieId: number): Promise => { + public removeMovie = async (tmdbId: number): Promise => { + const { id, title } = await this.getMovieByTmdbId(tmdbId); + + if (!id) { + logger.info(`[Radarr] Movie not in library, nothing to remove`, { + tmdbId, + }); + return; + } + try { - const { id, title } = await this.getMovieByTmdbId(movieId); await this.axios.delete(`/movie/${id}`, { params: { deleteFiles: true, @@ -278,9 +288,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { }); logger.info(`[Radarr] Removed movie ${title}`); } catch (e) { - throw new Error(`[Radarr] Failed to remove movie: ${e.message}`, { - cause: e, - }); + if (e?.response?.status === 404) { + logger.info(`[Radarr] Movie already removed from Radarr`, { + tmdbId, + }); + return; + } + throw e; } }; diff --git a/server/api/servarr/sonarr.test.ts b/server/api/servarr/sonarr.test.ts new file mode 100644 index 000000000..015b57e90 --- /dev/null +++ b/server/api/servarr/sonarr.test.ts @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it, mock } from 'node:test'; + +import type { AxiosInstance } from 'axios'; + +import SonarrAPI from '@server/api/servarr/sonarr'; + +function buildSonarr(): SonarrAPI { + return new SonarrAPI({ url: 'http://localhost:8989/api/v3', apiKey: 'test' }); +} + +function getAxios(sonarr: SonarrAPI): AxiosInstance { + return (sonarr as unknown as { axios: AxiosInstance }).axios; +} + +describe('SonarrAPI removeSeries', () => { + afterEach(() => mock.restoreAll()); + + it('removes the series when it exists in the library', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await sonarr.removeSeries(1234); + + assert.strictEqual(del.mock.callCount(), 1); + assert.strictEqual(del.mock.calls[0].arguments[0], '/series/9'); + }); + + it('does nothing when the series is not in the library', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ + data: [{ id: 0, title: 'Breaking Bad' }], + })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.doesNotReject(() => sonarr.removeSeries(1234)); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('rejects when the tvdbId is unknown to the lookup', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ data: [] })); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.rejects(() => sonarr.removeSeries(1234), /Series not found/); + assert.strictEqual(del.mock.callCount(), 0); + }); + + it('ignores a 404 when the series was already removed in Sonarr', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + mock.method(getAxios(sonarr), 'delete', async () => { + throw { response: { status: 404 } }; + }); + + await assert.doesNotReject(() => sonarr.removeSeries(1234)); + }); + + it('rethrows errors other than 404', async () => { + const sonarr = buildSonarr(); + mock.method(SonarrAPI.prototype, 'getSeriesByTvdbId', async () => ({ + id: 9, + title: 'Test Series', + })); + mock.method(getAxios(sonarr), 'delete', async () => { + throw { response: { status: 500 } }; + }); + + await assert.rejects(() => sonarr.removeSeries(1234)); + }); + + it('rethrows a 404 from the lookup instead of treating it as removed', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => { + throw { response: { status: 404 } }; + }); + const del = mock.method(getAxios(sonarr), 'delete', async () => ({})); + + await assert.rejects( + () => sonarr.removeSeries(1234), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 404 + ); + assert.strictEqual(del.mock.callCount(), 0); + }); +}); + +describe('SonarrAPI getSeriesByTvdbId', () => { + afterEach(() => mock.restoreAll()); + + it('rethrows a 401 from the lookup with the status intact', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => { + throw { response: { status: 401 } }; + }); + + await assert.rejects( + () => sonarr.getSeriesByTvdbId(1234), + (e: unknown) => + (e as { response?: { status?: number } }).response?.status === 401 + ); + }); + + it('throws "Series not found" when the lookup returns no results', async () => { + const sonarr = buildSonarr(); + mock.method(getAxios(sonarr), 'get', async () => ({ data: [] })); + + await assert.rejects(() => sonarr.getSeriesByTvdbId(1234), { + message: 'Series not found', + }); + }); +}); diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 33354e1cd..c5b4eec93 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -1,4 +1,5 @@ import logger from '@server/logger'; +import type { AxiosResponse } from 'axios'; import ServarrBase from './base'; export interface SonarrSeason { @@ -166,26 +167,27 @@ class SonarrAPI extends ServarrBase<{ } public async getSeriesByTvdbId(id: number): Promise { + let response: AxiosResponse; try { - const response = await this.axios.get('/series/lookup', { + response = await this.axios.get('/series/lookup', { params: { term: `tvdb:${id}`, }, }); - - if (!response.data[0]) { - throw new Error('Series not found'); - } - - return response.data[0]; } catch (e) { logger.error('Error retrieving series by tvdb ID', { label: 'Sonarr API', errorMessage: e.message, tvdbId: id, }); - throw new Error('Series not found', { cause: e }); + throw e; } + + if (!response.data[0]) { + throw new Error('Series not found'); + } + + return response.data[0]; } public async addSeries(options: AddSeriesOptions): Promise { @@ -410,9 +412,17 @@ class SonarrAPI extends ServarrBase<{ return newSeasons; } - public removeSeries = async (serieId: number): Promise => { + public removeSeries = async (tvdbId: number): Promise => { + const { id, title } = await this.getSeriesByTvdbId(tvdbId); + + if (!id) { + logger.info(`[Sonarr] Series not in library, nothing to remove`, { + tvdbId, + }); + return; + } + try { - const { id, title } = await this.getSeriesByTvdbId(serieId); await this.axios.delete(`/series/${id}`, { params: { deleteFiles: true, @@ -421,9 +431,13 @@ class SonarrAPI extends ServarrBase<{ }); logger.info(`[Sonarr] Removed series ${title}`); } catch (e) { - throw new Error(`[Sonarr] Failed to remove series: ${e.message}`, { - cause: e, - }); + if (e?.response?.status === 404) { + logger.info(`[Sonarr] Series already removed from Sonarr`, { + tvdbId, + }); + return; + } + throw e; } }; diff --git a/server/entity/Media.ts b/server/entity/Media.ts index a63003df6..304d80e8c 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -203,17 +203,21 @@ class Media { Object.assign(this, init); } - public resetServiceData(): void { - this.serviceId = null; - this.serviceId4k = null; - this.externalServiceId = null; - this.externalServiceId4k = null; - this.externalServiceSlug = null; - this.externalServiceSlug4k = null; - this.ratingKey = null; - this.ratingKey4k = null; - this.jellyfinMediaId = null; - this.jellyfinMediaId4k = null; + public resetServiceData(is4k?: boolean): void { + if (is4k === undefined || !is4k) { + this.serviceId = null; + this.externalServiceId = null; + this.externalServiceSlug = null; + this.ratingKey = null; + this.jellyfinMediaId = null; + } + if (is4k === undefined || is4k) { + this.serviceId4k = null; + this.externalServiceId4k = null; + this.externalServiceSlug4k = null; + this.ratingKey4k = null; + this.jellyfinMediaId4k = null; + } } @AfterLoad() diff --git a/server/routes/media.ts b/server/routes/media.ts index dbbe34aea..2295f0021 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -243,18 +243,18 @@ mediaRoutes.delete( } if (!serviceSettings) { - logger.warn( - `There is no default ${ - is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' - }/ server configured. Did you set any of your ${ - is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' - } servers as default?`, + const arrName = `${is4k ? '4K ' : ''}${isMovie ? 'Radarr' : 'Sonarr'}`; + logger.info( + `There is no default ${arrName} server configured. Did you set any of your ${arrName} servers as default?`, { label: 'Media Request', mediaId: media.id, } ); - return; + return next({ + status: 409, + message: `No ${arrName} server configured to delete media files`, + }); } let service; @@ -280,15 +280,27 @@ mediaRoutes.delete( throw new Error('TVDB ID not found'); } await (service as SonarrAPI).removeSeries(tvdbId); + + for (const season of media.seasons) { + season[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED; + } } + media[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED; + media.resetServiceData(is4k); + await mediaRepository.save(media); + return res.status(204).send(); } catch (e) { - logger.error('Something went wrong fetching media in delete request', { + if (e instanceof EntityNotFoundError) { + return next({ status: 404, message: 'Media not found' }); + } + logger.error('Something went wrong deleting media file', { label: 'Media', + mediaId: req.params.id, message: e.message, }); - next({ status: 404, message: 'Media not found' }); + next({ status: 500, message: 'Failed to delete media file' }); } } ); diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 13e32d4ef..a6fd5a174 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -8,6 +8,7 @@ import DownloadBlock from '@app/components/DownloadBlock'; import IssueBlock from '@app/components/IssueBlock'; import RequestBlock from '@app/components/RequestBlock'; import useSettings from '@app/hooks/useSettings'; +import useToasts from '@app/hooks/useToasts'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -64,6 +65,8 @@ const messages = defineMessages('components.ManageSlideOver', { removearr: 'Remove from {arr}', openarr4k: 'Open in 4K {arr}', removearr4k: 'Remove from 4K {arr}', + clearmediadataerror: 'Something went wrong while clearing the media data.', + removemediaerror: 'Something went wrong while removing the media.', downloadstatus: 'Downloads', markavailable: 'Mark as Available', mark4kavailable: 'Mark as Available in 4K', @@ -109,6 +112,7 @@ const ManageSlideOver = ({ }: ManageSlideOverMovieProps | ManageSlideOverTvProps) => { const { user: currentUser, hasPermission } = useUser(); const intl = useIntl(); + const { addToast } = useToasts(); const settings = useSettings(); const { data: watchData } = useSWR( settings.currentSettings.mediaServerType === MediaServerType.PLEX && @@ -126,18 +130,35 @@ const ManageSlideOver = ({ const deleteMedia = async () => { if (data.mediaInfo) { - await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); - revalidate(); - onClose(); + try { + await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + revalidate(); + onClose(); + } catch { + addToast(intl.formatMessage(messages.clearmediadataerror), { + appearance: 'error', + autoDismiss: true, + }); + } } }; const deleteMediaFile = async (is4k = false) => { if (data.mediaInfo) { - await axios.delete( - `/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}` - ); - await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + try { + await axios.delete( + `/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}` + ); + } catch (e) { + if (!axios.isAxiosError(e) || e.response?.status !== 404) { + addToast(intl.formatMessage(messages.removemediaerror), { + appearance: 'error', + autoDismiss: true, + }); + revalidate(); + return; + } + } revalidate(); onClose(); } diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index db1f2f216..b53d4541e 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -47,6 +47,7 @@ const messages = defineMessages('components.RequestList.RequestItem', { tvdbid: 'TheTVDB ID', unknowntitle: 'Unknown Title', removearr: 'Remove from {arr}', + removemediaerror: 'Something went wrong while removing the media.', profileName: 'Profile', }); @@ -354,10 +355,20 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { const deleteMediaFile = async () => { if (request.media) { - await axios.delete( - `/api/v1/media/${request.media.id}/file?is4k=${request.is4k}` - ); - await axios.delete(`/api/v1/media/${request.media.id}`); + try { + await axios.delete( + `/api/v1/media/${request.media.id}/file?is4k=${request.is4k}` + ); + } catch (e) { + if (!axios.isAxiosError(e) || e.response?.status !== 404) { + addToast(intl.formatMessage(messages.removemediaerror), { + autoDismiss: true, + appearance: 'error', + }); + revalidateList(); + return; + } + } revalidateList(); } }; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index c36d7d692..790380ad9 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -750,12 +750,12 @@ const UserList = () => { > {intl.formatMessage(messages.created)} - + {(data.results ?? []).length > 1 && (
- + +
+ + +
))} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index c456e21e5..d546f6782 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -305,6 +305,7 @@ "components.Login.validationservertyperequired": "Please select a server type", "components.Login.validationusernamerequired": "Username required", "components.ManageSlideOver.alltime": "All Time", + "components.ManageSlideOver.clearmediadataerror": "Something went wrong while clearing the media data.", "components.ManageSlideOver.downloadstatus": "Downloads", "components.ManageSlideOver.manageModalAdvanced": "Advanced", "components.ManageSlideOver.manageModalClearMedia": "Clear Data", @@ -329,6 +330,7 @@ "components.ManageSlideOver.plays": "{playCount, number} {playCount, plural, one {play} other {plays}}", "components.ManageSlideOver.removearr": "Remove from {arr}", "components.ManageSlideOver.removearr4k": "Remove from 4K {arr}", + "components.ManageSlideOver.removemediaerror": "Something went wrong while removing the media.", "components.ManageSlideOver.tvshow": "series", "components.MediaSlider.ShowMoreCard.seemore": "See More", "components.MetadataSelector.selectMetdataProvider": "Select a metadata provider", @@ -528,6 +530,7 @@ "components.RequestList.RequestItem.modifieduserdate": "{date} by {user}", "components.RequestList.RequestItem.profileName": "Profile", "components.RequestList.RequestItem.removearr": "Remove from {arr}", + "components.RequestList.RequestItem.removemediaerror": "Something went wrong while removing the media.", "components.RequestList.RequestItem.requested": "Requested", "components.RequestList.RequestItem.requesteddate": "Requested", "components.RequestList.RequestItem.seasons": "{seasonCount, plural, one {Season} other {Seasons}}",