From f78916bd41a20a3673aa3bc5c0e3c07d74217fd0 Mon Sep 17 00:00:00 2001 From: Ludovic Ortega Date: Tue, 28 Jul 2026 02:33:21 +0200 Subject: [PATCH 1/7] chore(helm): bump seerr to 3.4.0 (#3299) Signed-off-by: Ludovic Ortega --- charts/seerr-chart/Chart.yaml | 4 ++-- charts/seerr-chart/README.md | 2 +- devenv.nix | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 7b46f5ac3..3fa274024 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.8.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..33d032683 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.8.0](https://img.shields.io/badge/Version-3.8.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 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 = { From 40017e4ea7a11d84aed49e52fe77a81274bb74b5 Mon Sep 17 00:00:00 2001 From: Ludovic Ortega Date: Tue, 28 Jul 2026 10:48:39 +0200 Subject: [PATCH 2/7] chore(helm): add dnsConfig and dnsPolicy (#3254) Signed-off-by: Ludovic Ortega --- charts/seerr-chart/Chart.yaml | 2 +- charts/seerr-chart/README.md | 6 ++++- charts/seerr-chart/templates/statefulset.yaml | 13 ++++++++++ charts/seerr-chart/values.yaml | 25 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 3fa274024..be55ae8f0 100644 --- a/charts/seerr-chart/Chart.yaml +++ b/charts/seerr-chart/Chart.yaml @@ -3,7 +3,7 @@ kubeVersion: '>=1.23.0-0' name: seerr-chart description: Seerr helm chart for Kubernetes type: application -version: 3.8.0 +version: 3.9.0 # renovate: image=ghcr.io/seerr-team/seerr appVersion: 'v3.4.0' maintainers: diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index 33d032683..f624c2387 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -1,6 +1,6 @@ # seerr-chart -![Version: 3.8.0](https://img.shields.io/badge/Version-3.8.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) +![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 From 45f7a7a9abe43f5600db153b89750e9a2d76b916 Mon Sep 17 00:00:00 2001 From: Ludovic Ortega Date: Tue, 28 Jul 2026 17:12:28 +0200 Subject: [PATCH 3/7] ci: parallelize the action steps when possible (#3225) Signed-off-by: Ludovic Ortega --- .github/workflows/ci.yml | 82 ++-- .github/workflows/codeql.yml | 19 +- .github/workflows/create-tag.yml | 46 ++- .github/workflows/cypress.yml | 16 +- .github/workflows/detect-duplicate.yml | 13 +- .github/workflows/docs-deploy.yml | 16 +- .github/workflows/helm.yml | 113 ++--- .github/workflows/lint-helm-charts.yml | 40 +- .github/workflows/pr-validation.yml | 387 +++++++++--------- .github/workflows/preview.yml | 96 ++--- .github/workflows/rebuild-issue-index.yml | 15 +- .github/workflows/release.yml | 163 ++++---- .../workflows/renovate-helm-custom-hooks.yml | 25 +- .github/workflows/seerr-labeller.yml | 114 +++--- .github/workflows/test-docs.yml | 15 +- 15 files changed, 594 insertions(+), 566 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0938d3f5b..20abb7d29 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,31 +225,32 @@ 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: 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 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - 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: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - 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..59ad99b4a 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,41 +79,42 @@ 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: 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 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + 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: 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - 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: Extract metadata id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 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..6afb7491f 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,31 +115,32 @@ 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: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - 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: Log in to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - 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,29 +205,30 @@ 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Sign images run: | @@ -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 From 95e18ded1d6c7edf7e88f580e61266ef14b79475 Mon Sep 17 00:00:00 2001 From: Ludovic Ortega Date: Wed, 29 Jul 2026 09:35:28 +0200 Subject: [PATCH 4/7] ci: fix docker login race condition (#3303) --- .github/workflows/ci.yml | 22 +++++++++--------- .github/workflows/preview.yml | 25 ++++++++++---------- .github/workflows/release.yml | 44 +++++++++++++++++------------------ 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20abb7d29..ef0b36a17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -234,18 +234,18 @@ jobs: - 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Commit timestamp id: ts diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 59ad99b4a..b6cbba581 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -88,19 +88,6 @@ jobs: - 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Derive preview version from tag id: ver shell: bash @@ -111,6 +98,18 @@ jobs: echo "version=${VER}" >> "$GITHUB_OUTPUT" echo "Publishing preview version: ${VER}" + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Commit timestamp id: ts run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6afb7491f..943d542d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,18 +124,18 @@ jobs: - 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Commit timestamp id: ts @@ -217,18 +217,18 @@ jobs: - 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 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} - - name: Log in to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Sign images run: | From 8ea0c174ea6d23062a13f510924235f6d56b481e Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+fallenbagel@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:49:55 +0500 Subject: [PATCH 5/7] fix: handle already-removed media in radarr/sonarr delete (#3209) --- server/api/servarr/radarr.test.ts | 119 ++++++++++++++++++ server/api/servarr/radarr.ts | 40 ++++-- server/api/servarr/sonarr.test.ts | 119 ++++++++++++++++++ server/api/servarr/sonarr.ts | 40 ++++-- server/routes/media.ts | 22 ++-- src/components/ManageSlideOver/index.tsx | 36 ++++-- .../RequestList/RequestItem/index.tsx | 20 ++- src/i18n/locale/en.json | 3 + 8 files changed, 353 insertions(+), 46 deletions(-) create mode 100644 server/api/servarr/radarr.test.ts create mode 100644 server/api/servarr/sonarr.test.ts 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/routes/media.ts b/server/routes/media.ts index dbbe34aea..1626de95b 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; @@ -284,11 +284,15 @@ mediaRoutes.delete( 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..5be4d54c9 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,36 @@ 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}` + ); + await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + } 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..b3d309eee 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,21 @@ 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}` + ); + await axios.delete(`/api/v1/media/${request.media.id}`); + } 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/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}}", From 3ee6f9200cb040c7ad910294da3a9415dc84e71a Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+fallenbagel@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:49:56 +0500 Subject: [PATCH 6/7] fix: don't delete requests when removing media from arr (#3288) --- server/entity/Media.ts | 26 +++++++++++-------- server/routes/media.ts | 8 ++++++ src/components/ManageSlideOver/index.tsx | 1 - .../RequestList/RequestItem/index.tsx | 1 - 4 files changed, 23 insertions(+), 13 deletions(-) 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 1626de95b..2295f0021 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -280,8 +280,16 @@ 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) { if (e instanceof EntityNotFoundError) { diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 5be4d54c9..a6fd5a174 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -149,7 +149,6 @@ const ManageSlideOver = ({ await axios.delete( `/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}` ); - await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); } catch (e) { if (!axios.isAxiosError(e) || e.response?.status !== 404) { addToast(intl.formatMessage(messages.removemediaerror), { diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index b3d309eee..b53d4541e 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -359,7 +359,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { await axios.delete( `/api/v1/media/${request.media.id}/file?is4k=${request.is4k}` ); - await axios.delete(`/api/v1/media/${request.media.id}`); } catch (e) { if (!axios.isAxiosError(e) || e.response?.status !== 404) { addToast(intl.formatMessage(messages.removemediaerror), { From f926248e2daf660a452bbc16f36748adb240ab33 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <31414959+0xSysR3ll@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:02:37 +0200 Subject: [PATCH 7/7] fix(ui): keep user list action buttons sized with a single user (#3311) --- src/components/UserList/index.tsx | 57 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 29 deletions(-) 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 && (
- + +
+ + +
))}