Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
891a8ab
test: add organized regression suites and fix discovered bugs
4eh5xitv6787h645ebv Oct 5, 2026
28849e1
fix(poster-tags): never replace a token secret another instance alrea…
4eh5xitv6787h645ebv Oct 6, 2026
972bea8
fix(maintenance): keep the restore journal through write failures and…
4eh5xitv6787h645ebv Oct 6, 2026
1f948c1
fix(bookmarks): roll back failed bookmark saves
4eh5xitv6787h645ebv Oct 6, 2026
0600ca1
fix(shortcuts): match stored shortcuts whatever their modifier order
4eh5xitv6787h645ebv Oct 6, 2026
5a0d3cf
fix(item-details): show release dates on their calendar day west of UTC
4eh5xitv6787h645ebv Oct 6, 2026
9096fde
fix(calendar): parse and send calendar ranges with the invariant culture
4eh5xitv6787h645ebv Oct 6, 2026
71af93e
fix(poster-tags): match ICU when baku1926 sits beside alalc97 or a re…
4eh5xitv6787h645ebv Oct 6, 2026
f91bdf9
fix(spoiler-guard): stop pending-promotion sweeps with the service
4eh5xitv6787h645ebv Oct 6, 2026
4ade06d
test(cdn): skip the award-logo pacing delay in the refresh test
4eh5xitv6787h645ebv Oct 6, 2026
ce88c3b
test(backend): make vacuous and date-sensitive backend tests able to …
4eh5xitv6787h645ebv Oct 6, 2026
421b9aa
test(frontend): run modules like the bundle, pin zone and locale, add…
4eh5xitv6787h645ebv Oct 6, 2026
a7e6374
test(frontend): make vacuous frontend tests able to fail
4eh5xitv6787h645ebv Oct 6, 2026
b1713b1
test(browser): honour JE_BROWSER_PORT in the Playwright config
4eh5xitv6787h645ebv Oct 6, 2026
c7ada97
test(host): always clean up by name, label resources, retry taken sub…
4eh5xitv6787h645ebv Oct 6, 2026
9fc64a4
ci: allow the test-only Moq package in dependency review
4eh5xitv6787h645ebv Oct 6, 2026
5a7cc4b
ci(inventory): stop the inventory gate failing on ordinary edits
4eh5xitv6787h645ebv Oct 6, 2026
149b6b6
ci(security): scan both build targets for vulnerable packages
4eh5xitv6787h645ebv Oct 6, 2026
d8f4f3c
test: raise coverage floors to the measured values and fix stale comm…
4eh5xitv6787h645ebv Oct 6, 2026
4ddb41a
docs(tests): replace session logs with current, reproducible validation
4eh5xitv6787h645ebv Oct 6, 2026
8a2e021
Merge origin/main into t3code/create-regression-test-prompt
4eh5xitv6787h645ebv Oct 6, 2026
5a4414f
test(downloads): assert whole-row paging from main's grid-sized pages
4eh5xitv6787h645ebv Oct 6, 2026
63ec0a1
fix(bookmarks): run each user's bookmark mutations one at a time
4eh5xitv6787h645ebv Oct 6, 2026
26b21e3
fix(bookmarks): keep the old group when a migration save fails
4eh5xitv6787h645ebv Oct 6, 2026
f7a1b54
fix(bookmarks): report failed bookmark deletes and edits
4eh5xitv6787h645ebv Oct 6, 2026
9c700d4
fix(shortcuts): detect editor conflicts with any modifier order
4eh5xitv6787h645ebv Oct 6, 2026
b97607f
fix(release-dates): compare season air dates with the local day
4eh5xitv6787h645ebv Oct 6, 2026
e318eac
fix(poster-tags): promote a script variant only when it sorts first
4eh5xitv6787h645ebv Oct 6, 2026
a3c0d79
fix(maintenance): back off retries of a restore that keeps failing
4eh5xitv6787h645ebv Oct 6, 2026
36fb4e1
ci(regression): explain stale NuGet lock files on Dependabot PRs
4eh5xitv6787h645ebv Oct 6, 2026
49f689f
test(host): inspect networks one at a time when choosing a subnet
4eh5xitv6787h645ebv Oct 6, 2026
6488dd0
test(inventory): find scheduled tasks anywhere and document every tri…
4eh5xitv6787h645ebv Oct 6, 2026
3606435
test(poster): start offline parity runs from empty inputs
4eh5xitv6787h645ebv Oct 6, 2026
66c9511
test(downloads): watch the issues transport when the source is disabled
4eh5xitv6787h645ebv Oct 6, 2026
a7e417b
test(poster): exercise tile cache eviction and clearing
4eh5xitv6787h645ebv Oct 6, 2026
ee18d63
test(bootstrap): let deferred startup errors surface before teardown
4eh5xitv6787h645ebv Oct 6, 2026
fe40a90
test(bootstrap): enforce one top-level IIFE for unwrapped loading
4eh5xitv6787h645ebv Oct 6, 2026
a741d3a
test(privacy): keep a pending-promoter regression from hanging the run
4eh5xitv6787h645ebv Oct 6, 2026
0981c19
test(theme): pin the page clock in the daily-theme test
4eh5xitv6787h645ebv Oct 6, 2026
757f791
test(activity): verify feed user scoping and fresh authors per request
4eh5xitv6787h645ebv Oct 6, 2026
f150266
test(hidden-content): assert the user switch clears every hidden index
4eh5xitv6787h645ebv Oct 6, 2026
f36104d
test(spoiler-guard): assert preferences reset on a user switch
4eh5xitv6787h645ebv Oct 6, 2026
48c6c53
test(api): seed every secret-like setting in the config payload tests
4eh5xitv6787h645ebv Oct 6, 2026
960cc2c
test: make the calendar and review-rating user-switch checks bite
4eh5xitv6787h645ebv Oct 6, 2026
36c7ac1
test: cover four untested user-switch and cache guards
4eh5xitv6787h645ebv Oct 6, 2026
7315db6
docs(tests): drop session-log leftovers and record the final counts
4eh5xitv6787h645ebv Oct 6, 2026
a0a5a45
Merge remote-tracking branch 'origin/main' into t3code/review-pull-re…
4eh5xitv6787h645ebv Oct 6, 2026
45ed2b1
fix(tags): keep the current cache owner key in the legacy sweep
4eh5xitv6787h645ebv Oct 6, 2026
01b233a
fix(bookmarks): run episode backfill and delete-all in the mutation q…
4eh5xitv6787h645ebv Oct 6, 2026
31c68a4
fix(bookmarks): don't report an already-deleted bookmark as a failure
4eh5xitv6787h645ebv Oct 6, 2026
655826f
fix(maintenance): rewrite a journal that a failed save left only in m…
4eh5xitv6787h645ebv Oct 6, 2026
590e29a
test(url-guard): pin literal link-local, metadata names and the schem…
4eh5xitv6787h645ebv Oct 6, 2026
1cbac07
test(spoiler-guard): cover the default hide mode with decodable posters
4eh5xitv6787h645ebv Oct 6, 2026
279913a
test(analytics): make the consent checks bite with an overdue report
4eh5xitv6787h645ebv Oct 6, 2026
70cb0c8
test(activity): require user access to be configured before ItemIds
4eh5xitv6787h645ebv Oct 6, 2026
7d8d27f
test(privacy): close field-strip, marker, hidden-content and session …
4eh5xitv6787h645ebv Oct 6, 2026
5155242
test: tighten watchlist, arr status, Seerr permission, MDBList and sa…
4eh5xitv6787h645ebv Oct 6, 2026
851dda6
test(spoiler-guard): fail when a pending sweep outlives StopAsync
4eh5xitv6787h645ebv Oct 6, 2026
e2f04a0
test(frontend): cover bootstrap fallback, activity escaping, calendar…
4eh5xitv6787h645ebv Oct 6, 2026
dcf7f98
docs(tests): record round-3 coverage, known untested guards and final…
4eh5xitv6787h645ebv Oct 6, 2026
3002dff
Merge remote-tracking branch 'origin/main' into t3code/review-pull-re…
4eh5xitv6787h645ebv Oct 7, 2026
70cbc7f
fix(calendar): drop arr calendar responses that resolve after a user …
4eh5xitv6787h645ebv Oct 7, 2026
ebd7439
test(downloads): cover stale failures, late issues, Seerr details and…
4eh5xitv6787h645ebv Oct 7, 2026
8b55ae4
fix(tag-cache): apply the resolved user's Spoiler Guard state
4eh5xitv6787h645ebv Oct 7, 2026
a27ea9b
test(tag-cache): cover cross-user access and delta and played-state r…
4eh5xitv6787h645ebv Oct 7, 2026
9a795f0
fix(hidden-content): refresh the filter cache when playback clears a …
4eh5xitv6787h645ebv Oct 7, 2026
fda9388
fix(seerr): run the manual watchlist sync through the scheduled task'…
4eh5xitv6787h645ebv Oct 7, 2026
94425f1
fix(maintenance): keep a restore whose enable-path checkpoint failed
4eh5xitv6787h645ebv Oct 7, 2026
65da9d0
test: keep existing MDBList critic ratings and no-op tag-cache rebuilds
4eh5xitv6787h645ebv Oct 7, 2026
2e092ec
test(activity): make the disabled-category check reach the category s…
4eh5xitv6787h645ebv Oct 7, 2026
c965fcd
test(persistence): hand-shake the concurrent reader instead of relyin…
4eh5xitv6787h645ebv Oct 7, 2026
4c937d7
test(tags): keep the timestamp key in the legacy-stem collision case
4eh5xitv6787h645ebv Oct 7, 2026
33cbcfd
test(bookmarks): run queued delete-all and backfill across an account…
4eh5xitv6787h645ebv Oct 7, 2026
60285f9
test(seerr): run the request-button outcomes through the 4K split but…
4eh5xitv6787h645ebv Oct 7, 2026
1d456c8
test(seerr): cover the season modal, request modals and status mapping
4eh5xitv6787h645ebv Oct 7, 2026
dd9e813
test(more-info): cover a failed TV-request refresh, stale and current
4eh5xitv6787h645ebv Oct 7, 2026
39d96e8
test(shortcuts): keep a saved rebind or disable over the plugin defau…
4eh5xitv6787h645ebv Oct 7, 2026
c62d19f
docs(tests): record round-4 coverage and final counts
4eh5xitv6787h645ebv Oct 7, 2026
0605fee
fix(seerr): sync the watchlist from the first Seerr URL that answers
4eh5xitv6787h645ebv Oct 7, 2026
9936039
fix(watchlist): record processed watchlist items under the per-user f…
4eh5xitv6787h645ebv Oct 7, 2026
2a33985
test(seerr): assert the manual watchlist sync's whole response
4eh5xitv6787h645ebv Oct 7, 2026
cd30795
fix(seerr): update only the refreshed section's buttons on a results …
4eh5xitv6787h645ebv Oct 7, 2026
ac17271
test(seerr): pin request bodies, restored labels and modal options
4eh5xitv6787h645ebv Oct 7, 2026
60afc73
test(maintenance): a failed checkpoint stops the remaining restores
4eh5xitv6787h645ebv Oct 7, 2026
96b3719
test: cover injected page encoding, same-day maintenance windows and …
4eh5xitv6787h645ebv Oct 7, 2026
192b72f
test(tag-cache): assert both revalidated deltas carry no entries
4eh5xitv6787h645ebv Oct 7, 2026
547c766
test(downloads): a late history response cannot unhide the new accoun…
4eh5xitv6787h645ebv Oct 7, 2026
88139b8
test(persistence): count only reads that began after the saves started
4eh5xitv6787h645ebv Oct 7, 2026
8b0b965
docs(tests): record round-5 coverage and final counts
4eh5xitv6787h645ebv Oct 7, 2026
7d4fc3b
test(frontend): raise the test timeout to 120 s since Node 22 applies…
4eh5xitv6787h645ebv Oct 7, 2026
dee6f00
docs(tests): list processed-watchlist-items.json as a user-scoped file
4eh5xitv6787h645ebv Oct 7, 2026
dd1fce8
docs(watchlist): say which Seerr callers fall back across configured …
4eh5xitv6787h645ebv Oct 7, 2026
83ac32d
test(spoiler): assert protected images are actually blurred and follo…
4eh5xitv6787h645ebv Oct 7, 2026
4720027
test(requests): a results refresh matches each button's exact id
4eh5xitv6787h645ebv Oct 7, 2026
aca23eb
test(api): check the dev-mode bundle's own header and that it is rebuilt
4eh5xitv6787h645ebv Oct 7, 2026
ded03ff
test(activity): retention drops the oldest timestamp, not the lowest key
4eh5xitv6787h645ebv Oct 7, 2026
8ba26ab
test(watch-providers): only the length guard rejects a valid but over…
4eh5xitv6787h645ebv Oct 7, 2026
2f3a61a
test(release-dates): the configured region wins, then US, over list o…
4eh5xitv6787h645ebv Oct 7, 2026
32145f5
test(reviews): regular viewers can edit and delete only their own review
4eh5xitv6787h645ebv Oct 7, 2026
19b9227
docs(tests): record round-6 coverage and final counts
4eh5xitv6787h645ebv Oct 7, 2026
770a766
fix(auto-requests): read Seerr release dates as Gregorian in every cu…
4eh5xitv6787h645ebv Oct 7, 2026
77dc41c
fix(people, requests): parse and format ISO dates with the invariant …
4eh5xitv6787h645ebv Oct 7, 2026
4d7c29a
test(browser): pause the clock so only runFor expires the toast
4eh5xitv6787h645ebv Oct 7, 2026
6d2bdb1
test(browser): keep the language panel's reload from firing mid-test
4eh5xitv6787h645ebv Oct 7, 2026
05fa5db
test(browser): check the request modal's controls by their labels
4eh5xitv6787h645ebv Oct 7, 2026
47f1359
docs(tests): record round-7 culture coverage and final counts
4eh5xitv6787h645ebv Oct 7, 2026
3a860e0
Merge remote-tracking branch 'origin/main' into t3code/review-pull-re…
4eh5xitv6787h645ebv Oct 7, 2026
5f63c96
fix(tests): prefix inventory routes with each controller's class route
4eh5xitv6787h645ebv Oct 7, 2026
1a4725b
docs(tests): list Catch Up as an untested area in the coverage matrix
4eh5xitv6787h645ebv Oct 7, 2026
b8179d9
fix(requests): count a sequel as released from local midnight again
4eh5xitv6787h645ebv Oct 7, 2026
75633fa
docs(tests): record one full validation run after the main merge
4eh5xitv6787h645ebv Oct 7, 2026
5ee740d
Merge remote-tracking branch 'origin/main' into t3code/review-pull-re…
4eh5xitv6787h645ebv Oct 8, 2026
ec0fe13
docs(tests): regenerate the inventory after merging main
4eh5xitv6787h645ebv Oct 8, 2026
8e64c47
test(host): always test the newest 10.11 and 12 Jellyfin releases
4eh5xitv6787h645ebv Oct 8, 2026
e2271a5
docs(tests): record the host run on Jellyfin 10.11.11 and 12.2.0
4eh5xitv6787h645ebv Oct 8, 2026
f0c47a5
Merge branch 'main' into t3code/create-regression-test-prompt
n00bcodr Oct 8, 2026
07f2d19
Merge main into regression test branch
n00bcodr Oct 8, 2026
6e51c0d
fix(tests): sort the production inventory by path so it matches on Li…
n00bcodr Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: '9.0.x'
dotnet-version: '10.0.x'

- if: matrix.build-mode == 'manual'
name: Build
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ jobs:
# the rest are MIT/Apache-2.0). `deny-licenses` is deprecated upstream in favor
# of this allow-list model: https://github.com/actions/dependency-review-action/issues/997
allow-licenses: MIT, Apache-2.0, GPL-3.0-only
# Allow the CI-only TruffleHog GitHub Action despite its AGPL license classification.
allow-dependencies-licenses: pkg:githubactions/trufflesecurity/trufflehog
# Allow the CI-only TruffleHog GitHub Action despite its AGPL license classification,
# and the test-only Moq mocking library (BSD-3-Clause; tests/backend, never shipped).
allow-dependencies-licenses: pkg:githubactions/trufflesecurity/trufflehog, pkg:nuget/Moq
179 changes: 179 additions & 0 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: Regression tests

on:
pull_request:
push:
branches: [main, master]
schedule:
- cron: '23 3 * * 2'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: regression-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# Separate job: a stale inventory fails this check without skipping the tests.
inventory:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- run: python3 tests/inventory/generate.py --check

backend:
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
target: [jf10, jf12]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: |
9.0.x
10.0.x
# Dependabot bumps package versions but cannot update the per-target lock files
# (packages.jf10/jf12.lock.json), so the locked restore below would stop with NU1004.
# Regenerate this target's lock files and fail with the fix instead.
- name: Check NuGet lock files (Dependabot)
if: github.event.pull_request.user.login == 'dependabot[bot]'
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
for project in Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.csproj tests/backend/JE.Tests.csproj; do
dotnet restore "$project" -p:JellyfinTarget="$TARGET" --force-evaluate
done
if ! git diff --quiet -- '*.lock.json'; then
git --no-pager diff -- '*.lock.json'
echo "::error::The $TARGET NuGet lock files are out of date after this dependency update. Regenerate both targets' lock files as described in CONTRIBUTING.md (NuGet lock files) and push them to this branch."
exit 1
fi
- run: python3 tests/run.py backend --target ${{ matrix.target }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: backend-${{ matrix.target }}
path: |
artifacts/regression/*.log
artifacts/regression/summary.json
artifacts/regression/backend/
retention-days: 14

frontend:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '22.14.0'
cache: npm
- run: npm ci
- run: python3 -m unittest discover -s tests/runner/tests -v
- run: python3 tests/run.py frontend
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: frontend-coverage
path: |
artifacts/regression/*.log
artifacts/regression/summary.json
artifacts/frontend-coverage/
coverage/
retention-days: 14

browser:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '22.14.0'
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium firefox
- run: python3 tests/run.py browser
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: browser-results
path: |
artifacts/regression/*.log
artifacts/regression/summary.json
artifacts/browser/
playwright-report/
test-results/
retention-days: 14

host:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
target: [jf10, jf12]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: '10.0.x'
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '22.14.0'
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: python3 tests/run.py host --target ${{ matrix.target }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: host-${{ matrix.target }}
path: |
artifacts/regression/*.log
artifacts/regression/summary.json
artifacts/regression/host/
retention-days: 14

extended:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: |
9.0.x
10.0.x
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
# Matches the ICU version used to generate poster language tables.
node-version: '26.2.0'
cache: npm
- run: npm ci
- run: python3 tests/run.py poster --artifacts artifacts/regression/poster-run
- run: python3 tests/run.py mutation --artifacts artifacts/regression/mutation-run
if: always()
- run: python3 tests/run.py history --artifacts artifacts/regression/history-run
if: always()
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: extended-results
path: |
artifacts/regression/*.log
artifacts/regression/summary.json
artifacts/regression/poster-run/
artifacts/regression/mutation-run/
artifacts/regression/history-run/
retention-days: 14
47 changes: 38 additions & 9 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,49 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: '9.0.x'
dotnet-version: |
9.0.x
10.0.x

# Both build targets: jf12 (net10.0, the default) and jf10 (net9.0). JellyfinTarget is read
# from the environment as an MSBuild property. A restore or listing failure fails the step.
- name: List vulnerable packages
run: dotnet list Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.csproj package --vulnerable --include-transitive 2>&1 | tee vulnerable-packages.txt
shell: bash
run: |
set -euo pipefail
for target in jf12 jf10; do
echo "::group::$target"
JellyfinTarget=$target dotnet restore Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.csproj
JellyfinTarget=$target dotnet list Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.csproj package \
--vulnerable --include-transitive --no-restore --format json > "vulnerable-$target.json"
cat "vulnerable-$target.json"
echo "::endgroup::"
done

- name: Check for vulnerabilities
shell: bash
run: |
if grep -q "has the following vulnerable packages" vulnerable-packages.txt; then
echo "::error::Vulnerable packages found!"
cat vulnerable-packages.txt
exit 1
else
echo "No vulnerable packages found."
fi
python3 - vulnerable-jf12.json vulnerable-jf10.json <<'EOF'
import json, sys
failed = False
for path in sys.argv[1:]:
report = json.load(open(path))
projects = report.get('projects') or []
if not projects:
print(f'::error::{path}: no project was scanned')
failed = True
for problem in report.get('problems') or []:
print(f"::error::{path}: {problem.get('text', problem)}")
failed = True
for project in projects:
for framework in project.get('frameworks') or []:
for package in (framework.get('topLevelPackages') or []) + (framework.get('transitivePackages') or []):
print(f"::error::{path}: {framework.get('framework')} {package.get('id')} {package.get('resolvedVersion')} has known vulnerabilities")
failed = True
if failed:
sys.exit(1)
print('No vulnerable packages found for jf12 or jf10.')
EOF

# SARIF upload for GitHub Security tab
upload-sarif:
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,11 @@ scripts/deploy*

# Local test output (poster tag harnesses and captures)
/.engineering-artifacts/

# Regression infrastructure manifests are versioned; generated reports are not.
!/package.json
!/package-lock.json
/artifacts/
/coverage/
/playwright-report/
/test-results/
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,24 @@ Adding a client module:

## ✅ CI Checks

Every PR runs a few automated checks (GitHub Actions, `.github/workflows/`). There are no automated tests to run locally - these are all static checks:
Pull requests run automated backend regressions for both Jellyfin targets, JavaScript behavioral tests with coverage, and browser tests, alongside the static checks below. See [Regression testing](tests/README.md) for setup, commands, reports, and current coverage gaps.

| Check | What it does | Reproduce locally |
|---|---|---|
| **CodeQL Advanced** | Static analysis for both the C# backend and the JS frontend, looking for common security/correctness bug patterns | Not practical to run locally; check the PR's "Files changed" annotations if it flags something |
| **Dependency Review** | Flags newly-introduced dependencies with known vulnerabilities or incompatible licenses | Only relevant if your PR changes `.csproj` package references |
| **Security Scan** | Scans the diff for accidentally-committed secrets (API keys, tokens, credentials) with TruffleHog | `git diff` your changes yourself before pushing if you're unsure |
| **Translation Checks** | For any locale file you touched under `js/locales/`, verifies it has valid JSON and the same key set as `en.json` (no missing/extra keys) | Diff your changed locale file's keys against `js/locales/en.json` by hand, or just keep the two in sync as you edit |
| **NuGet lock files** | The backend regression jobs restore in locked mode from per-target lock files (`packages.jf12.lock.json`, `packages.jf10.lock.json`) beside the plugin and test projects, so a changed package reference fails with NU1004 until they are regenerated. Dependabot can't update these files, so its NuGet PRs fail a dedicated check with this fix | For each `JellyfinTarget` (`jf12`, `jf10`), run `dotnet restore Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.csproj -p:JellyfinTarget=<target> --force-evaluate` and the same for `tests/backend/JE.Tests.csproj`, then commit the four lock files |
| **Production inventory** | Part of the regression workflow: fails when `tests/docs/production-inventory.json` no longer matches the plugin. Ordinary edits and translation updates don't change it. It does change when you add, remove or rename a production file, locale, HTTP route (or its method), configuration property, scheduled task (any class implementing `IScheduledTask`) or storage literal; when a route's following attributes such as `[Authorize]` change; when a configuration property's type, default value or constructor assignment changes; or when a build target's `TargetFramework` or `JellyfinVersion` changes | Run `python3 tests/inventory/generate.py` and commit the regenerated file (`--check` only verifies it) |

Two more workflows exist but aren't part of the PR gate: **Check Unused Translation Keys** and **OpenSSF Scorecard** are both maintainer-triggered/scheduled, not run against your PR - a scorecard badge or unused-key report you might see elsewhere in the repo isn't something your PR needs to pass.

## 🧪 Testing

Before submitting a PR, ensure you've tested:
After installing the prerequisites in [Regression testing](tests/README.md), run `python3 tests/run.py fast` for local feedback or `python3 tests/run.py all` for the complete suite. The full run includes disposable real Jellyfin hosts; it never uses your personal server. Automated coverage is recorded in the [coverage matrix](tests/docs/coverage-matrix.md).

For behavior outside automated coverage, also verify:

- [ ] Feature works as expected
- [ ] No console errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,14 +608,17 @@

try
{
var parsed = JsonConvert.DeserializeObject<AllReviewsStore>(json);
if (parsed == null)
// Missing/null collections and null rows are corruption, not an
// empty store. Do not let a subsequent write erase evidence.
var parsed = new AllReviewsStore { Reviews = null! };
JsonConvert.PopulateObject(json, parsed, new JsonSerializerSettings { CheckAdditionalContent = true });
if (parsed.Reviews == null || parsed.Reviews.Values.Any(review => review == null))
{
if (throwOnCorruption)
{
_logger.Error("reviews.json deserialized to null; refusing to write over it.");
_logger.Error("reviews.json contains an invalid review store; refusing to write over it.");
BackupCorruptFileUnlocked(filePath);
throw new InvalidDataException("reviews.json deserialized to null.");
throw new InvalidDataException("reviews.json contains an invalid review store.");
}
return new AllReviewsStore();
}
Expand Down Expand Up @@ -908,22 +911,55 @@
SaveUserConfiguration(userId.ToString(), "processed-watchlist-items.json", items);
}

/// Records a processed watchlist item for a user unless the same TMDB id and media
/// type is already recorded. Runs under the per-user file lock, so the scheduled
/// sync, a manual sync and the watchlist monitor cannot drop each other's entries.
/// Returns true when the item was added; a failed write is logged, not thrown, so
/// one user's unreadable file never stops a sync or monitor pass.
public bool MarkWatchlistItemProcessed(Guid userId, int tmdbId, string mediaType, string source)
Comment thread
n00bcodr marked this conversation as resolved.
Dismissed
{
try
{
return RmwUserConfiguration<ProcessedWatchlistItems>(userId.ToString(), "processed-watchlist-items.json", items =>
{
if (items.Items.Any(p => p.TmdbId == tmdbId && p.MediaType == mediaType))
{
return 0;
}

items.Items.Add(new ProcessedWatchlistItem
{
TmdbId = tmdbId,
MediaType = mediaType,
ProcessedAt = System.DateTime.UtcNow,
Source = source
});
return 1;
}) > 0;
}
catch (Exception ex)
{
_logger.Error($"Error recording processed watchlist item {mediaType}:{tmdbId} for user {userId}: {ex.Message}");
return false;
}
Comment thread
n00bcodr marked this conversation as resolved.
Dismissed
}

/// Cleans up old processed watchlist items (older than specified days).
public void CleanupOldProcessedWatchlistItems(Guid userId, int daysToKeep = 365)
{
try
{
var items = GetProcessedWatchlistItems(userId);
var cutoffDate = System.DateTime.UtcNow.AddDays(-daysToKeep);
var removed = RmwUserConfiguration<ProcessedWatchlistItems>(userId.ToString(), "processed-watchlist-items.json", items =>
{
var originalCount = items.Items.Count;
items.Items = items.Items.Where(item => item.ProcessedAt > cutoffDate).ToList();
return originalCount - items.Items.Count;
});

var originalCount = items.Items.Count;
var itemsToKeep = items.Items.Where(item => item.ProcessedAt > cutoffDate).ToList();

if (itemsToKeep.Count != originalCount)
if (removed > 0)
{
items.Items = itemsToKeep;
SaveProcessedWatchlistItems(userId, items);
_logger.Info($"Cleaned up {originalCount - itemsToKeep.Count} old processed watchlist items for user {userId}");
_logger.Info($"Cleaned up {removed} old processed watchlist items for user {userId}");
}
}
catch (Exception ex)
Expand Down
Loading
Loading