Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
197 commits
Select commit Hold shift + click to select a range
e8e5103
feat(notifications): name the server and profile in every relay push …
Quick104 Oct 13, 2026
4761680
fix(api): stop counting and logging cancelled requests as server fail…
blurbery Oct 13, 2026
71ea481
feat(metadata): offer local sidecar artwork in the admin image picker…
blurbery Oct 13, 2026
3583af8
fix(playback): keep the requested version when only its subtitle is b…
blurbery Oct 13, 2026
0768b47
fix(jellycompat): offer a route to clients whose H.264 codec rules ar…
Brady3035 Oct 13, 2026
f095e0c
fix(notifications): send a real notification as the admin test push (…
Quick104 Oct 13, 2026
eecb7f5
fix(nodes): sign worker requests instead of sending the shared secret…
Quick104 Oct 13, 2026
2716982
feat(web): filter the image picker by provider and list local art fir…
Quick104 Oct 13, 2026
aa8b53e
feat(sections): split home Recently Added into movies and TV rows (#2…
Quick104 Oct 13, 2026
3bbd6d3
fix(plugins): time out stalled catalog and package fetches (#2361)
Rhainland Oct 13, 2026
11a197d
fix(metadata): match the personal collection key when merging duplica…
Rhainland Oct 13, 2026
aeaffac
fix(web): keep Continue Watching landscape when it holds only movies …
Rhainland Oct 13, 2026
4b2e921
fix(playback): keep web subtitles in sync after a seek on offset file…
Quick104 Oct 13, 2026
3280a88
fix(mdblist): prevent API key leakage in transport errors (fixes #692…
rangoDJ Oct 13, 2026
3d80e2b
fix(catalog): add system media control artwork to WatchDetail (fixes …
rangoDJ Oct 13, 2026
2359f6c
chore(hooks): catch gofmt and golangci-lint issues in pre-commit (#865)
rangoDJ Oct 13, 2026
2d746e8
perf(subtitles): reuse cached embedded extracts for AI translation so…
rangoDJ Oct 13, 2026
e21aaed
fix(web): scope realtime query invalidation by item (#857)
rangoDJ Oct 13, 2026
a044982
feat(playback): serve copy remux from a complete keyframe playlist (#…
rangoDJ Oct 13, 2026
d9c79f3
fix(access): remove deleted libraries from access groups and accounts…
Quick104 Oct 13, 2026
80c71c2
fix(autoscan): accept Sonarr SeriesDelete and Radarr MovieDelete webh…
Quick104 Oct 13, 2026
590bfcf
fix(web): name a route's Sonarr metadata language in its summary (#2411)
Quick104 Oct 13, 2026
22e7189
fix(trickplay): correct HDR previews, decode on NVIDIA, and repair mi…
Quick104 Oct 13, 2026
864d7d1
fix(apiv2): describe notification reason_flags as notification data (…
bulderi Oct 13, 2026
fb6c444
fix(web): keep debouncing admin log filters after Reconnect (#2405)
bulderi Oct 13, 2026
22489b1
fix(opslog): store logged errors as their text instead of {} (#1658)
dannyshafer Oct 13, 2026
c13a669
feat(admin): show total egress for the selected window (#2406)
Brady3035 Oct 13, 2026
88ea8d2
fix(web): block saving users while a limit field is invalid on a hidd…
dannyshafer Oct 13, 2026
626d29f
fix(collections): reject unknown visibility on admin create and updat…
dannyshafer Oct 13, 2026
c3e0875
fix(observability): scrape the main server's metrics listener in the …
dannyshafer Oct 13, 2026
c05ef0d
fix(jellycompat): give episodes their show's logo through ParentLogoI…
Brady3035 Oct 13, 2026
e094f94
fix(jellycompat): return no items when no Ids resolve (#2354)
Rhainland Oct 13, 2026
92f9e29
fix(web): label Atmos tracks from their audio profile (#2309)
blurbery Oct 13, 2026
a2b8bfe
fix(playback): replan around a transcode or proxy node that stopped a…
Rhainland Oct 13, 2026
e9eef85
fix(web): offer pause, resume and stop on the Activity page (#2281)
Rhainland Oct 13, 2026
1a0916c
fix(web): keep AI transcription in the spoken language when translati…
Rhainland Oct 13, 2026
78c642a
fix(web): let inherited policy selects fit their label (#1668)
dannyshafer Oct 13, 2026
fee3df3
feat(access): let access groups set the household profile limit (#1801)
Tobechukwu-Njoku Oct 13, 2026
3173b22
feat(web): remove admin color overrides, fonts, and custom CSS (#2413)
Quick104 Oct 14, 2026
6144c45
fix(playback): show an administrator pause or resume as soon as the p…
Rhainland Oct 14, 2026
1d52e31
fix(metadata): stop keeping real TBD-titled episodes in refresh debt …
dannyshafer Oct 14, 2026
11e3569
fix(web): reopen a progressive remux after a long pause (#2416)
Quick104 Oct 14, 2026
b83e887
fix(personal-lists): notify only when a favorites or watchlist write …
Quick104 Oct 14, 2026
493d1d7
fix(naming): treat a bare Season folder as a season folder (#1660)
dannyshafer Oct 14, 2026
3311bab
fix(s3client): return an error when object deletes fail (#1665)
dannyshafer Oct 14, 2026
e807f25
feat(web): add a playback speed control to the video player (#2429)
Quick104 Oct 14, 2026
ef68312
fix(playback): keep windows from a cached .sup on the playback timeli…
Quick104 Oct 14, 2026
706943e
feat(web): draw embedded PGS subtitles in the browser (#1923)
Quick104 Oct 14, 2026
5434b04
feat(web): restore defaults on every library page at once (#2460)
Quick104 Oct 14, 2026
7dc6c40
fix(adminjob): allow canceling image cache cleanup jobs (#1669)
dannyshafer Oct 14, 2026
4ae03a3
feat(web): add an opt-in browser decoder for files the browser cannot…
Quick104 Oct 14, 2026
4aa0134
fix(tonemap): pin the software tone-map peak so Dolby Vision isn't da…
neurekadev Oct 14, 2026
009b47d
fix(playback): scale QSV tone-map frames before OpenCL, not after (#2…
neurekadev Oct 14, 2026
e21984f
fix(scanner): keep extras folders in a show folder from becoming seri…
Quick104 Oct 14, 2026
bbef279
fix(playback): composite QSV HDR bitmap subtitles on OpenCL (#2449)
neurekadev Oct 14, 2026
a86d05b
fix(watchstate): clear watched episodes without a file when unwatchin…
Quick104 Oct 14, 2026
668a518
fix(playback): move a transcode off a dead node instead of failing ev…
Rhainland Oct 14, 2026
7765f3f
Show a combined Cast & Crew row with sharper headshots (#1834)
blurbery Oct 14, 2026
27310a5
fix(jellycompat): name the client from PlaybackInfo when the stream r…
Tobechukwu-Njoku Oct 14, 2026
fbcd030
fix(web): stop the Search page dropping typed characters (#2216)
blurbery Oct 14, 2026
5a56490
fix(web): stop saying S3 is required for artwork-storage features (#1…
dannyshafer Oct 14, 2026
f8b16ef
feat(web): fold watch-provider sync settings behind a Settings toggle…
Rhainland Oct 14, 2026
f361d69
feat(markers): share detected credits automatically (#1822)
Tobechukwu-Njoku Oct 14, 2026
3b98a5e
fix(web): keep the download storage API out of the launch bundle (#2204)
blurbery Oct 14, 2026
fb990a1
docs(admin-api): say the node bandwidth cap applies to proxies only (…
Rhainland Oct 14, 2026
1dcfd82
fix(web): select a choice card by clicking anywhere on it (#1973)
zZebrahz Oct 14, 2026
976da54
feat(catalog): add ratings to v2 episode listings (#1829)
olivertgwalton Oct 14, 2026
c6ecf77
fix(watchsync): sync favorites and watchlists when two items share a …
Quick104 Oct 14, 2026
d8091f1
feat(web): reorganize admin and user settings around the sidebar (#2398)
Quick104 Oct 14, 2026
6270409
fix(downloads): count v2 download file transfers as viewer egress (#2…
Rhainland Oct 14, 2026
b0a417c
fix(metadata): keep AI provenance when a refresh or item merge rewrit…
Quick104 Oct 14, 2026
99d5623
fix(metadata): start a forced translation instead of reusing a runnin…
Rhainland Oct 14, 2026
c8885b4
fix(metadata): keep AI item translations through a scheduled refresh …
Quick104 Oct 14, 2026
ed8adc3
fix(metadata): never let a forced AI re-translation replace provider …
Rhainland Oct 14, 2026
7f5c607
fix(web): refresh catalog views when the metadata language changes (#…
Rhainland Oct 14, 2026
efdc731
feat(catalog): mark machine-translated descriptions (#2207)
Rhainland Oct 14, 2026
0cf610d
fix(trickplay): publish seek previews for XviD/DivX AVI and interlace…
Joloxx9 Oct 14, 2026
ece16f8
fix(metadata): translate the episodes a page lists on view (#2208)
Rhainland Oct 14, 2026
8f28583
fix(trickplay): publish seek previews for H.264 files whose keyframes…
Joloxx9 Oct 14, 2026
f346117
fix(ratelimit): make Redis Retry-After cover a full minute window (#2…
Quick104 Oct 14, 2026
43aa3e5
fix(ratelimit): make in-memory Retry-After cover an empty minute buck…
Quick104 Oct 14, 2026
5e3af66
fix(metadata): limit on-view translation to the viewer's missing lang…
Rhainland Oct 14, 2026
ea3c10a
fix(scanner,markers): stop stalled ffprobe and ffmpeg runs at a deadl…
blurbery Oct 14, 2026
8cad25c
perf(trickplay): start Matroska list entries at their keyframes (#2484)
Quick104 Oct 14, 2026
7fadc3c
fix(playback): bound the Cues a Matroska keyframe index reads (#2492)
Quick104 Oct 14, 2026
207e3f2
fix(sections): localize Home and library section cards (#2209)
Rhainland Oct 14, 2026
846ba62
feat(metadata): prewarm a library's translations from web admin (#2212)
Rhainland Oct 14, 2026
7b680d9
fix(recommendations): make the recommendation pipeline correct, isola…
Quick104 Oct 14, 2026
9d649fb
fix(web): stop offering favourites and the watchlist on episode cards…
blurbery Oct 14, 2026
a1c1450
fix(webhook-sync): ignore unsupported Emby and Plex events before req…
blurbery Oct 14, 2026
b0b65de
feat(scanner): read sidecar subtitle languages in any position and fl…
blurbery Oct 14, 2026
d34a7af
feat(libraries): let admins mark unmatched items as intentionally unm…
blurbery Oct 15, 2026
3387f12
fix(requests): send requests to routers that need no API key (#2439)
blurbery Oct 15, 2026
fa98e0c
fix(profiles): delete all of a profile's own rows with the profile
Quick104 Oct 15, 2026
bd9d59e
Revert "fix(profiles): delete all of a profile's own rows with the pr…
Quick104 Oct 15, 2026
4307984
feat(progress): reset a partly watched item's resume position (#2440)
blurbery Oct 15, 2026
2137fe1
test(contracts): update request ID in ignore-unmatched 404 fixture (#…
Quick104 Oct 15, 2026
9d852a1
feat(devices): let a profile name its devices (#2520)
Quick104 Oct 15, 2026
c1c4b92
fix(subtitles): keep provider error text out of v1 search warnings (#…
Quick104 Oct 15, 2026
ed8cb1b
fix(artwork): keep provider artwork URLs off the server's network (#2…
Quick104 Oct 15, 2026
685344a
fix(tonemap): transcode HLG files tagged with BT.709 primaries (#2531)
Quick104 Oct 15, 2026
9be41fa
fix(profiles): delete all of a profile's own rows with the profile (#…
Quick104 Oct 15, 2026
0e2f5b2
feat(discovery): let signed-in clients use the server's LAN address (…
Quick104 Oct 15, 2026
236b0bc
fix(autoscan): don't record a failed poll's error on a source reset d…
blurbery Oct 15, 2026
2659222
perf(scanner): finalise variants from narrow rows with batched writes…
blurbery Oct 15, 2026
13aeee6
fix(autoscan): build the failed-first-poll subtest with a PollFailure
Quick104 Oct 15, 2026
d94a970
fix(watch-party): anchor commands at execute_at and converge drift by…
neurekadev Oct 15, 2026
b44f552
feat(playback): measure web bandwidth instead of trusting Chrome's 10…
Quick104 Oct 15, 2026
d4088da
fix(config): refuse Redis settings the server cannot start with (#2215)
blurbery Oct 15, 2026
e58824b
fix(userstore): keep a played mark made in the same second as an unpl…
blurbery Oct 15, 2026
1973af6
fix(web): fail over or recover when the browser decoder's WebGPU pres…
Quick104 Oct 15, 2026
48ae561
fix(web): claim HDR10 on iPhone Safari's hardware HEVC decoder (#2542)
Quick104 Oct 15, 2026
0e8277f
fix(web): copy 10-bit frames that Safari's WebGPU import misreads (#2…
Quick104 Oct 15, 2026
c65eb60
fix(playback): rebuild header-authenticated sessions after an API res…
Quick104 Oct 15, 2026
98805d5
feat(web): add and manage profiles from Who's watching (#2539)
Quick104 Oct 15, 2026
b46514c
fix(web): keep the activity actions inside the stream table (#2543)
RXWatcher Oct 15, 2026
fedd671
ci: a pull request runs only the jobs it can reach, on pinned actions…
darthShadow Oct 15, 2026
13fcc4d
fix(plugins): report refused automatic updates from Check Plugin Upda…
Rhainland Oct 15, 2026
46f9e0d
fix(plugins): show a non-resident plugin that cannot start as Failed …
Rhainland Oct 15, 2026
9a2b950
feat(recommendations): let a profile mark titles Not interested (#2503)
Quick104 Oct 15, 2026
f8d4020
fix(plugins): keep starting when one plugin fails to preload (#2226)
Rhainland Oct 15, 2026
5eb3164
perf(sections): cap Continue Watching at its item limit and drop an u…
Quick104 Oct 16, 2026
f6ca8e9
fix(notifications): lead episode push titles with the series name (#2…
Quick104 Oct 16, 2026
c78d233
feat(settings): let a profile show new episodes first in Continue Wat…
Quick104 Oct 16, 2026
3d9a002
ci: wider static checks fail new problems and leave existing debt alo…
darthShadow Oct 16, 2026
d1ddfbe
fix(ratelimit): check per-IP and key limits before the global budget …
Rhainland Oct 16, 2026
f78cb27
feat(notifications): show which series can notify a profile (#2566)
Quick104 Oct 16, 2026
22db7e8
fix(metadata): serve provider artwork directly when artwork caching i…
Quick104 Oct 16, 2026
461c09c
fix(events): apply the acting-admin rule and PIN proof to realtime so…
Rhainland Oct 16, 2026
99c04a9
fix(historyimport): bound memory per import run, not just per respons…
Rhainland Oct 16, 2026
1e0b14a
feat!: drop Trakt support (#2586)
Quick104 Oct 16, 2026
af96b4a
fix(web): describe the hook-rule exceptions in the notified series list
Quick104 Oct 16, 2026
c414af7
fix(metadata): keep a tagged movie apart from a title it shares one I…
Rhainland Oct 16, 2026
fc9e10a
feat(notifications): let a profile mute one series (#2589)
Quick104 Oct 16, 2026
1a0b0f7
fix(deps): update golang.org/x/net to v0.60.0 (#2590)
Quick104 Oct 16, 2026
4372e1e
fix(ci): compare repository checks against the tested merge's target …
Quick104 Oct 16, 2026
6e78fbc
ci(lint): enable gocritic's bug-finding checks and three small linter…
Quick104 Oct 16, 2026
166f832
chore(web): give the remaining eslint directives a reason (#2593)
Quick104 Oct 16, 2026
dfe1dab
fix(lint): resolve nilerr, sqlclosecheck, bodyclose, and nilness find…
Quick104 Oct 16, 2026
01e01b0
refactor(lint): remove dead stores and needless conversions flagged b…
Quick104 Oct 16, 2026
9cc3091
fix(settings): save subtitle appearance and timing to the profile (#2…
Rhainland Oct 16, 2026
21cdfa1
fix(notifications): send episode notifications only to the recipient …
Quick104 Oct 16, 2026
c1fcf12
fix: report a query that fails mid-result instead of returning partia…
Quick104 Oct 16, 2026
f2347ac
fix(notifications): key episode notification state by account and pro…
Quick104 Oct 16, 2026
df107b7
fix(notifications): keep destination management on the acting account…
Quick104 Oct 16, 2026
2fc804e
perf(sections): read watch history once for Returning Shows
Quick104 Oct 16, 2026
27e32d2
revert: undo the Returning Shows query change pushed directly to main…
Quick104 Oct 16, 2026
a0997ac
perf(sections): read watch history once for Returning Shows (#2614)
Quick104 Oct 17, 2026
e8be18e
fix(jellycompat): list every genre when /Genres has no Limit (#2556)
blurbery Oct 17, 2026
75a4ee6
fix(deploy): keep the main server's prepared downloads across contain…
Quick104 Oct 17, 2026
fada421
fix(downloads): accept a completion reported after the prepared file …
Quick104 Oct 17, 2026
2a59248
fix(web): keep catalog grids filled while a realtime refresh reloads …
Quick104 Oct 17, 2026
552a7bf
feat(artwork): cache originals up to 4K and add wider rungs (#2626)
Quick104 Oct 17, 2026
2fe4518
feat(apiv2): let clients size each artwork slot by drawn width (#2627)
Quick104 Oct 17, 2026
19e0528
fix(recommendations): size recommendation artwork like home sections …
Quick104 Oct 17, 2026
35eadaf
fix(watchsync): export a plugin's whole watched-history backlog in on…
Quick104 Oct 17, 2026
d2302c6
chore(deps): bump silo-plugin-sdk to v0.27.0 (#2632)
Quick104 Oct 17, 2026
5d1ab2b
fix(watchsync): check the run deadline on every history export page (…
Quick104 Oct 17, 2026
d121950
feat(playback): make Auto best-effort direct play and retire Original…
Quick104 Oct 17, 2026
bb6fddb
fix(web): base the player's max resolution on the decoder, not the sc…
Quick104 Oct 17, 2026
35ef973
feat(playback): carry embedded text subtitles inside transcoded HLS s…
Rhainland Oct 17, 2026
130ee70
fix(plugins): run connection checks for marker provider plugins (#2653)
Quick104 Oct 17, 2026
1cbeb9f
feat(markers): send the episode's own provider IDs and title to marke…
Quick104 Oct 17, 2026
7db7d35
fix(search): require every word in typo matches and rank leading titl…
Quick104 Oct 17, 2026
88b633c
fix(playback): align subtitle rendition cues when the source clock do…
Quick104 Oct 17, 2026
ef74c5e
perf(web): keep the events socket through short tab switches (#2546)
blurbery Oct 17, 2026
f2af203
ci(web): prune stale react-hooks/refs suppression for RealtimeEventsP…
Quick104 Oct 17, 2026
c4170f3
perf(database): read and write pgvector values in binary (#2550)
blurbery Oct 17, 2026
707f1ae
perf(apiv2): keep sendfile on v2 playback and theme audio routes (#2548)
blurbery Oct 17, 2026
ecd0cc6
perf(recommendations): keep taste candidate scans on the vector index…
blurbery Oct 17, 2026
db9fed1
perf(catalog): reindex a person's titles only when their name changes…
blurbery Oct 18, 2026
b430099
ci: fix Go lint findings and the person refresh pacing test flake (#2…
Quick104 Oct 18, 2026
2bf1fb0
feat(web): group the detail page's More menu and move admin actions i…
Quick104 Oct 18, 2026
6ab2e85
feat(web): show Forgot password? below the password field (#2658)
Quick104 Oct 18, 2026
df31546
feat(database): name the code site and SQL shape behind query cost (#…
darthShadow Oct 18, 2026
448a8f7
feat(sections): curated holiday lists with vibes for Seasonal Picks (…
Quick104 Oct 18, 2026
5484420
fix(subtitles): keep the SubDL account key out of search result IDs (…
Quick104 Oct 18, 2026
76d948a
fix(web): fit the Edit row dialog on phone screens (#2672)
Quick104 Oct 18, 2026
e3ba8a5
feat(collections): remove the 500-title limit on synced lists (#2647)
Rhainland Oct 18, 2026
373692b
feat(web): offer playback speed with the browser decoder (#2670)
Quick104 Oct 18, 2026
a434bbc
fix(jellycompat): return every named item when Ids has no Limit (#2668)
Quick104 Oct 18, 2026
e1e9cfa
Merge upstream Silo-Server/silo-server a434bbc2f2 into main
JonahMMay Oct 18, 2026
e817025
Fix sync audit findings: duplicate itemTypeSeries, restore embeddings…
JonahMMay Oct 18, 2026
88ca953
Fix CI round 1: recommendations card artwork and stubs, regenerate pn…
JonahMMay Oct 18, 2026
dff61ad
Regenerate generated files after the upstream sync
Oct 18, 2026
fe47304
Fix CI round 2: CastCarousel duplicate, port transcode HEAD tests to …
JonahMMay Oct 19, 2026
258d535
Fix CI round 3: web test expectations after the settings and nav rest…
JonahMMay Oct 19, 2026
c860ee0
Restore qualityRungLabel dropped in the playback-info merge
JonahMMay Oct 19, 2026
9457592
Pin prairie-plugin-sdk to the merged upstream sync (0aec721)
JonahMMay Oct 19, 2026
ed4362f
Regenerate apiv2 fixtures; fix apns_topic rebrand in the push fixture
JonahMMay Oct 19, 2026
1ae35a7
sync: adopt upstream artwork ladder plus Prairie w200 TV rung; drop d…
JonahMMay Oct 19, 2026
c8169bb
fixtures: restore LF admin_policy_vendor fixture (Docker regen embedd…
JonahMMay Oct 19, 2026
3f8732c
ci: gate lint on synced upstream lines, cover titleutil, align subtit…
JonahMMay Oct 19, 2026
6427c45
sync: restore Prairie's TV-tuned artwork ladder
JonahMMay Oct 19, 2026
994dec1
ci: gate sync-PR lint on lines new to both main and the merged upstre…
JonahMMay Oct 19, 2026
67cadd8
admin settings: drop the Redis check seam the removed save guard used
JonahMMay Oct 19, 2026
218c6c9
lint: errors.New for the constant proxy-node refusal (perfsprint)
JonahMMay Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 4 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ set -euo pipefail

# Resolve via git toplevel so the hook works from any cwd (e.g., when
# git invokes hooks from within a non-root subdirectory).
"$(git rev-parse --show-toplevel)/scripts/check-local-path-leaks.sh" --cached
repo_root="$(git rev-parse --show-toplevel)"

"$repo_root/scripts/check-local-path-leaks.sh" --cached
"$repo_root/scripts/check-go-lint.sh" --cached
6 changes: 6 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# These five trusted-tool checkouts use job.workflow_sha, which the runner
# provides but actionlint 1.7.12 does not yet include in its job context model.
paths:
.github/workflows/trusted-linux-ci.yml:
ignore:
- '^property "workflow_sha" is not defined in object type \{check_run_id: number; container: \{id: string; network: string\}; services: \{string => \{id: string; network: string; ports: \{string => string\}\}\}; status: string\}$'
20 changes: 17 additions & 3 deletions .github/actions/ci-tools/go-checks.mk
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# tests, each job on its own runner.
# LINT_BASE_REF, LINT_BASE_MODE, CONTRACT_BASE_REF, and TEST_GOFLAGS come from
# the workflow.
.PHONY: lint contracts tests
.PHONY: lint contracts tests test-dependencies

# Router recovery must hold over the whole tree, including inherited lines.
lint:
Expand All @@ -17,14 +17,28 @@ contracts:
$(MAKE) -j1 -f Makefile verify-settings-bindings verify-playback-fixtures verify-route-inventory
$(MAKE) -j1 -f Makefile verify-migration-ledger verify-apiv2-openapi CONTRACT_GO_TESTS=0
git fetch --no-tags origin "+refs/heads/$${CONTRACT_BASE_REF}:refs/remotes/origin/$${CONTRACT_BASE_REF}"
$(MAKE) -j1 -f Makefile verify-v1-route-freeze BASE_REF="origin/$${CONTRACT_BASE_REF}"
$(MAKE) -j1 -f Makefile verify-apiv2-contract CONTRACT_GO_TESTS=0 BASE_REF="origin/$${CONTRACT_BASE_REF}"

# Go's test cache cannot see files a test's child process reads, so these
# packages always rerun; the grep fails when a new test starts go, git, or node
# outside them or the packages whose subprocess inputs are immutable.
UNCACHED := cmd/prairie internal/jellycompat internal/pluginhost internal/plugins
UNCACHED := cmd/prairie internal/jellycompat internal/pluginhost internal/plugins scripts/ci/sizeratchet scripts/ci/migrationcheck
# subprocess-manifest: scripts/ci/subprocess-tests.json
# deferred: no recorded occurrence; retain UNCACHED and the subprocess guard.
IMMUTABLE := internal/contractledger internal/contractspec internal/debugserver
tests:
test-dependencies:
@variants="$$(go list -deps -test -f '{{.ImportPath}}|{{.ForTest}}' ./internal/auth/ ./internal/api/handlers/)" || exit $$?; \
[ -n "$$variants" ] || { echo "::error::test dependency metadata is empty"; exit 1; }; \
printf '%s\n' "$$variants" | awk -F '|' '\
$$2 != "" { split($$1, name, " "); \
if ((name[1] == "github.com/prairie-server/prairie-server/internal/api/handlers" || \
name[1] == "github.com/prairie-server/prairie-server/internal/apiv2") && name[1] != $$2) { \
print "::error::lower-layer external test recompiles " $$0; bad=1 \
} \
} END { exit bad }'

tests: test-dependencies
GOFLAGS="$${TEST_GOFLAGS}" $(MAKE) -f Makefile test-go
@want="$$(printf '%s\n' $(UNCACHED) $(IMMUTABLE) | sort -u)"; \
got="$$(git grep -l -E '(exec\.Command(Context)?\(([^,]+, )?"(go|git|node)"|LookPath\("(go|git|node)"\))' -- '*_test.go' | sed 's@/[^/]*$$@@' | sort -u)"; \
Expand Down
32 changes: 30 additions & 2 deletions .github/actions/ci-tools/select-jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
# Run on every event regardless of what the diff touched.
ALWAYS_JOBS = ("repo-checks",)
SHA = re.compile(r"[0-9a-fA-F]{40}\Z")
# These are embedded by contracts/api/v2 and consumed by the Go ledger,
# scenario and compatibility gates. Web generation consumes openapi/fixtures.
GO_ONLY_CONTRACTS = frozenset("contracts/api/v2/" + name for name in (
"migration.json", "migration.schema.json", "offline-routes.txt",
"breaking-approvals.json", "breaking-approvals.schema.json",
))


def all_jobs(reason):
Expand All @@ -29,6 +35,16 @@ def classify(paths):
if (not parts or path.startswith("/") or ".." in parts
or any(ord(char) < 32 for char in path)):
return all_jobs("unrecognized path")
if (path == ".github/evidence.yml"
or (path.startswith(".github/ISSUE_TEMPLATE/") and path.endswith((".yml", ".yaml", ".md")))
or (path.endswith(".md") and path.startswith((".github/", "scripts/", "cmd/")))):
continue
if (path in GO_ONLY_CONTRACTS
or path in {"scripts/ci/db-pins.txt", "scripts/ci/db-contracts.txt"}
or (path.startswith("internal/apiv2/") and path.endswith(".go"))
or (path.startswith("cmd/") and not path.startswith("cmd/settingsgen/"))):
go = True
continue
# These feed both generated Go artifacts and their Web bindings, or
# change how validation runs. Keep the entire gate for them.
# Schemas under docs include executable fixtures whose tests glob all
Expand Down Expand Up @@ -90,7 +106,9 @@ def plan(event_name, event):
# count a rename once, so a larger local count is still complete.
if len(paths) < count:
return all_jobs("incomplete changed-file count")
return classify(paths)
result = classify(paths)
result["paths"] = paths
return result
except (KeyError, TypeError, ValueError, UnicodeError, OSError, subprocess.CalledProcessError):
return all_jobs("PR comparison unavailable")

Expand Down Expand Up @@ -120,10 +138,20 @@ def main():
if len(sys.argv) != 2 or sys.argv[1] not in {"plan", "check"}:
raise SystemExit("expected plan or check")
if sys.argv[1] == "check":
errors = check_results(json.loads(os.environ["CI_JOB_RESULTS"]))
needs = json.loads(os.environ["CI_JOB_RESULTS"])
errors = check_results(needs)
if errors:
cancelled_jobs = sorted(job for job, outcome in needs.items()
if outcome.get("result") == "cancelled")
if cancelled_jobs:
print("Cancelled CI jobs: " + ", ".join(cancelled_jobs))
for error in errors:
print("::error::" + error)
# A distinct nonzero status preserves cancellation evidence without
# turning an unexpectedly cancelled required child into success.
if cancelled_jobs and all(error.endswith(" ended with cancelled")
for error in errors):
return 2
return 1
print("Every selected CI job passed.")
return 0
Expand Down
167 changes: 165 additions & 2 deletions .github/actions/ci-tools/test_select_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -119,6 +120,18 @@ def test_understood_inputs_choose_only_required_groups(self):
("pkg/client/client.go", True, False),
("example.go", True, False),
("docs/example.go", True, False),
("internal/apiv2/registry.go", True, False),
("internal/apiv2/registry_test.go", True, False),
("cmd/prairie/main.go", True, False),
("scripts/ci/db-pins.txt", True, False),
("scripts/ci/db-contracts.txt", True, False),
("contracts/api/v2/migration.json", True, False),
("contracts/api/v2/migration.schema.json", True, False),
("contracts/api/v2/offline-routes.txt", True, False),
("contracts/api/v2/breaking-approvals.json", True, False),
(".github/actions/README.md", False, False),
(".github/evidence.yml", False, False),
(".github/ISSUE_TEMPLATE/bug-report.yml", False, False),
]
for number, (path, go, web) in enumerate(cases):
with self.subTest(path=path):
Expand All @@ -133,13 +146,11 @@ def test_understood_inputs_choose_only_required_groups(self):
def test_shared_generation_and_validation_inputs_require_both_groups(self):
cases = [
".github/workflows/ci.yml",
".github/actions/README.md",
"scripts/check-local-path-leaks.sh",
"contracts/api/v2/openapi.json",
"contracts/api/v2/README.md",
"contracts/settings/v1/conformance.json",
"tools/generate.py",
"internal/apiv2/registry.go",
"internal/apiv2/docsui/LICENSE",
"internal/settingscontract/contract.go",
"cmd/settingsgen/main.go",
Expand All @@ -164,6 +175,11 @@ def test_shared_generation_and_validation_inputs_require_both_groups(self):
self.repo.commit()
self.assert_groups(self.repo.plan(), True, True)

def test_plan_reports_triggering_paths(self):
self.repo.write("internal/apiv2/registry.go")
self.repo.commit()
self.assertEqual(self.repo.plan().get("paths"), ["internal/apiv2/registry.go"])

def test_markdown_inside_schema_inputs_is_not_skipped_as_documentation(self):
for path in (
"docs/design/schemas/README.md",
Expand Down Expand Up @@ -517,6 +533,153 @@ def test_check_cli_returns_failure_and_identifies_the_failed_job(self):
if failed:
self.assertIn("::error::go-integration ended with failure", run.stdout)

def test_check_cli_distinguishes_cancelled_children_from_failures(self):
needs = self.results()
needs["go-test"]["result"] = "cancelled"
run = subprocess.run(
[sys.executable, str(SCRIPT), "check"], capture_output=True, text=True,
env=dict(os.environ, CI_JOB_RESULTS=json.dumps(needs)),
)
self.assertEqual(run.returncode, 2, run.stdout + run.stderr)
self.assertIn("Cancelled CI jobs: go-test", run.stdout)
self.assertNotIn("Every selected CI job passed", run.stdout)
needs["web"]["result"] = "failure"
run = subprocess.run(
[sys.executable, str(SCRIPT), "check"], capture_output=True, text=True,
env=dict(os.environ, CI_JOB_RESULTS=json.dumps(needs)),
)
self.assertEqual(run.returncode, 1, run.stdout + run.stderr)
self.assertIn("web ended with failure", run.stdout)


class WorkflowGateTests(unittest.TestCase):
root = SCRIPT.parents[3]

def pin_script(self):
workflow = (self.root / ".github/workflows/trusted-linux-ci.yml").read_text()
marker = " - name: Check action SHA pins\n"
self.assertIn(marker, workflow, "Repository checks must enforce immutable actions")
step = workflow.split(marker, 1)[1].split(" - ", 1)[0]
script = step.split(" run: |\n", 1)[1]
return "\n".join(line[10:] for line in script.splitlines() if line.strip())

def test_pin_gate_rejects_nested_mutable_and_malformed_references(self):
script = self.pin_script()
with tempfile.TemporaryDirectory() as directory:
repo = Repository(Path(directory) / "repo")
for reference in ("actions/cache@v4", "owner/repo/workflow.yml@main",
"actions/checkout@" + "a" * 39,
"actions/checkout@" + "z" * 40):
with self.subTest(reference=reference):
repo.write(".github/actions/nested/action.yml", "steps:\n - uses: " + reference + "\n")
repo.commit()
run = subprocess.run(["bash", "-c", script], cwd=repo.path,
capture_output=True, text=True)
self.assertNotEqual(run.returncode, 0, run.stdout + run.stderr)
self.assertIn(reference, run.stdout + run.stderr)

def check_pin_fixture(self, step, expected):
workflow = ("name: pins\non: push\npermissions: {}\njobs:\n test:\n"
" runs-on: ubuntu-24.04\n steps:\n"
" - uses: actions/checkout@" + "a" * 40 + "\n"
" " + step + "\n")
if shutil.which("actionlint"):
valid = subprocess.run(["actionlint", "-oneline", "-"], input=workflow,
cwd=self.root, capture_output=True, text=True)
self.assertEqual(valid.returncode, 0, valid.stdout + valid.stderr)
with tempfile.TemporaryDirectory() as directory:
repo = Repository(Path(directory) / "repo")
repo.write(".github/workflows/test.yml", workflow)
repo.commit()
run = subprocess.run(["bash", "-c", self.pin_script()], cwd=repo.path,
capture_output=True, text=True)
self.assertEqual(run.returncode, expected,
f"{step}: gate returned {run.returncode}, want {expected}\n"
+ run.stdout + run.stderr)

def test_pin_gate_rejects_flow_mapping(self):
self.check_pin_fixture("- { uses: actions/checkout@v4 }", 1)

def test_pin_gate_rejects_quoted_key(self):
self.check_pin_fixture('- "uses": actions/checkout@v4', 1)

def test_pin_gate_accepts_flow_mapping_without_uses(self):
self.check_pin_fixture("- run: echo ok\n env: { A: b }", 0)

def test_pin_gate_accepts_pinned_block_mapping(self):
self.check_pin_fixture("- uses: actions/checkout@" + "b" * 40, 0)

def check_workflow_pin_fixture(self, reference, expected):
workflow = ("name: pins\non: push\npermissions: {}\njobs:\n test:\n"
" uses: " + reference + "\n")
if shutil.which("actionlint"):
valid = subprocess.run(["actionlint", "-oneline", "-"], input=workflow,
cwd=self.root, capture_output=True, text=True)
self.assertEqual(valid.returncode, 0, valid.stdout + valid.stderr)
with tempfile.TemporaryDirectory() as directory:
repo = Repository(Path(directory) / "repo")
repo.write(".github/workflows/test.yml", workflow)
repo.commit()
run = subprocess.run(["bash", "-c", self.pin_script()], cwd=repo.path,
capture_output=True, text=True)
self.assertEqual(run.returncode, expected,
f"{reference}: gate returned {run.returncode}, want {expected}\n"
+ run.stdout + run.stderr)

def test_pin_gate_accepts_same_repository_workflows_at_main(self):
for filename in ("trusted-linux-ci.yml", "another-workflow.yaml"):
with self.subTest(filename=filename):
self.check_workflow_pin_fixture(
"Prairie-Server/prairie-server/.github/workflows/" + filename + "@main", 0)

def test_pin_gate_rejects_other_repository_workflows_at_main(self):
for repository in ("another-owner/prairie-server", "Prairie-Server/another-repo"):
with self.subTest(repository=repository):
self.check_workflow_pin_fixture(
repository + "/.github/workflows/trusted-linux-ci.yml@main", 1)

def test_pin_gate_rejects_same_repository_workflows_at_other_refs(self):
for ref in ("feature", "v1.0.0"):
with self.subTest(ref=ref):
self.check_workflow_pin_fixture(
"Prairie-Server/prairie-server/.github/workflows/trusted-linux-ci.yml@" + ref, 1)

def test_pin_gate_accepts_immutable_and_local_references(self):
script = self.pin_script()
with tempfile.TemporaryDirectory() as directory:
repo = Repository(Path(directory) / "repo")
repo.write(".github/workflows/test.yml", "steps:\n - uses: actions/checkout@" + "a" * 40 + " # reviewed release\n - uses: ./.github/actions/setup-go\n")
repo.write(".github/actions/nested/action.yml", "steps:\n - uses: ./.github/actions/local\n")
repo.commit()
run = subprocess.run(["bash", "-c", script], cwd=repo.path,
capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
run = subprocess.run(["bash", "-c", script], cwd=directory,
capture_output=True, text=True)
self.assertNotEqual(run.returncode, 0, "A missing Git input must fail")

def test_full_tree_tool_targets_exist(self):
run = subprocess.run(["make", "-n", "verify-actionlint", "verify-shellcheck"],
cwd=self.root, capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
self.assertIn("1.7.12", run.stdout)
self.assertIn("0.11.0", run.stdout)

def test_workflow_and_shell_tools_reject_seeded_defects(self):
missing = [tool for tool in ("actionlint", "shellcheck") if not shutil.which(tool)]
if missing:
self.skipTest("required tools not on PATH: " + ", ".join(missing))
workflow = "name: test\non: push\njobs:\n test:\n runs-on: ubuntu-24.04\n steps:\n - run: echo ok\n"
for contents, expected in ((workflow, 0), (workflow.replace("echo ok", "echo ${{ unknown.value }}"), 1)):
run = subprocess.run(["actionlint", "-oneline", "-"], input=contents,
cwd=self.root, capture_output=True, text=True)
self.assertEqual(run.returncode, expected, run.stdout + run.stderr)
for contents, expected in (("#!/bin/bash\nprintf '%s\\n' \"$1\"\n", 0),
("#!/bin/bash\nprintf '%s\\n' $1\n", 1)):
run = subprocess.run(["shellcheck", "-s", "bash", "-"], input=contents,
cwd=self.root, capture_output=True, text=True)
self.assertEqual(run.returncode, expected, run.stdout + run.stderr)


if __name__ == "__main__":
unittest.main()
10 changes: 6 additions & 4 deletions .github/actions/save-go-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ inputs:
runs:
using: composite
steps:
# if: !cancelled() && inputs.job-status != 'cancelled'
# Declined for compiled-cache saving: failed main runs keep the success-only policy.
- name: Prune unused Go build cache entries
if: success() && inputs.job-status == 'success' && runner.environment == 'github-hosted' && github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request'
if: success() && inputs.job-status == 'success' && runner.environment == 'github-hosted' && github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' && github.event_name != 'schedule'
shell: bash
run: |
cache_dir="$(go env GOCACHE)"
find "$cache_dir" -mindepth 2 -maxdepth 2 \( -name '*-a' -o -name '*-d' \) ! -newermt '@0' -exec rm -rf -- {} +

- name: Save the Go build cache
if: success() && inputs.job-status == 'success' && runner.environment == 'github-hosted' && github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request'
uses: actions/cache/save@v4
if: success() && inputs.job-status == 'success' && runner.environment == 'github-hosted' && github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' && github.event_name != 'schedule'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/go-build
key: ${{ inputs.cache-key }}
Expand All @@ -46,7 +48,7 @@ runs:

- name: Save the shared Go module cache
if: ${{ !cancelled() && inputs.job-status != 'cancelled' && runner.environment == 'github-hosted' && github.ref_name == github.event.repository.default_branch && github.event_name != 'pull_request' && inputs.module-cache-key != '' && inputs.module-cache-hit != 'true' }}
uses: actions/cache/save@v4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/go/pkg/mod
key: ${{ inputs.module-cache-key }}
Loading
Loading