Compare commits

...

435 commits

Author SHA1 Message Date
shenlong
ffc83eae36
chore: more dart lints (#30665)
Some checks failed
Static Code Analysis / pre-job (push) Has been cancelled
Test / pre-job (push) Has been cancelled
Test / ShellCheck (push) Has been cancelled
Test / OpenAPI Clients (push) Has been cancelled
Test / SQL Schema Checks (push) Has been cancelled
CLI Build / Docker (push) Has been cancelled
Docker / Re-Tag ML (push) Has been cancelled
Docker / Re-Tag Server (push) Has been cancelled
Docker / Build and Push ML (push) Has been cancelled
Docker / Build and Push Server (push) Has been cancelled
Docker / Mirror to Docker Hub (push) Has been cancelled
Docker / Docker Build & Push Server Success (push) Has been cancelled
Docker / Docker Build & Push ML Success (push) Has been cancelled
Docs build / Docs Build (push) Has been cancelled
Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
Test / Scripts unit tests (push) Has been cancelled
Test / Test & Lint Server (push) Has been cancelled
Test / Unit Test CLI (push) Has been cancelled
Test / Unit Test CLI (Windows) (push) Has been cancelled
Test / Lint Web (push) Has been cancelled
Test / Test Web (push) Has been cancelled
Test / Test i18n (push) Has been cancelled
Test / End-to-End Lint (push) Has been cancelled
Test / Medium Tests (Server) (push) Has been cancelled
Test / End-to-End Tests (Server & CLI) (push) Has been cancelled
Test / End-to-End Tests (Web) (push) Has been cancelled
Test / End-to-End Tests Success (push) Has been cancelled
Test / Unit Test Mobile (push) Has been cancelled
Test / Unit Test ML (push) Has been cancelled
Test / .github Files Formatting (push) Has been cancelled
The lints are regrouped and sorted. Also, added the following new lints:
- unnecessary_ignore
- parameter_assignments
- avoid_unused_constructor_parameters
- tighten_type_of_initializing_formals
- only_throw_errors
- deprecated_consistency
- unnecessary_statements

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-15 16:55:45 +05:30
shenlong
bd55f7e73a
chore: pump flutter_lints to 6.0 (#30664)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-15 16:55:45 +05:30
Ben Beckford
f9c05af45f
fix(server): migration order (#30774) 2026-08-14 21:10:28 +00:00
Jason Rasmussen
342a9472c4
refactor: duplicate e2e (#30773) 2026-08-14 21:00:13 +00:00
Jason Rasmussen
14241ac7bc
chore(server): remove unused code (#30772)
chore(server): remove unnused code
2026-08-14 16:58:14 -04:00
Daniel Dietzler
6a61901e79
fix: shared link create validation (#30762) 2026-08-14 22:04:32 +02:00
Jason Rasmussen
b19c4a591e
chore(web): remove unused code (#30768) 2026-08-14 16:04:12 -04:00
cmdPromptCritical
66d010381e
fix(mobile): prevent snapping to center on pinch-to-zoom release (#29207) (#29343)
Co-authored-by: cmdpromptcritical <cmdpromptcritical@github.com>
2026-08-14 12:30:28 -07:00
Adam Gastineau
190a939af7
fix(mobile): disable iOS smart quotes/dashes in email/password inputs (#30767)
* fix(mobile): disable iOS smart quotes/dashes in email/password inputs

* Remove enum prefixes
2026-08-14 19:21:39 +00:00
shenlong
652ef8a427
refactor: server capabilities (#30663)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-14 07:53:12 -05:00
Ben Beckford
447cc40a50
feat: workflow logging (#29878)
* feat: workflow logging

* chore: update sql and openapi patch

* chore: include id in workflow log entries

* chore(server): update sql and lint

* chore(mobile): remove openapi files

* chore(web): lint

* chore: clean up workflow logging

* chore(server): add runId to workflow logs

* chore(server): store workflow results as enum
2026-08-13 19:18:24 -07:00
ihmcsm
af33a78d18
fix: typos in debug log messages (#30742) 2026-08-13 09:30:19 +00:00
Ben Beckford
a939561e70
feat: workflow asset tag trigger/filter/action (#29043)
* feat: workflow asset tag trigger and filter

* feat(web): show tag names in workflow editor

* fix(web): tag picker in schema config editor

* fix: invalid plugin manifest

* chore: update workflow method wrapper type

* chore: update tag filter method declaration

* feat: workflow action to add tags to assets
2026-08-12 16:26:03 -07:00
Adam Gastineau
b82d480552
fix(mobile): prevent iOS status bar scroll to top during transitions (#30717) 2026-08-12 12:57:45 -07:00
Adam Gastineau
db9e7c20d7
chore(mobile): add a single mise checkout command for reoccuring tasks (#30737)
* chore(mobile): add a single mise checkout command for reoccuring tasks

* Fix .flutter-plugins-dependencies path

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2026-08-12 11:30:05 -07:00
shenlong
ff5da0f84f
chore: immich_mobile path in openapi pubspec (#30643)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 23:00:08 +05:30
Mees Frensel
2a1691868e
chore: use import.meta.dirname instead of __dirname (#30738) 2026-08-12 18:01:30 +02:00
shenlong
1c3a5cf508
chore: remove old people provider (#30662)
* chore: remove old people provider

* refactor: rename driftGetAllPeopleProvider

* minor fixes

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 21:12:12 +05:30
shenlong
303a9f15b1
refactor: reactive driftGetAllPeopleProvider (#30660)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 21:12:12 +05:30
shenlong
52edcc0c74
refactor: unify person model (#30659)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 21:12:11 +05:30
shenlong
943c11c019
chore: label local only delete as trash (#30726)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 10:02:08 -04:00
shenlong
ce705dcc06
fix: riverpod reactivity regression from lint fixes (#30734)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-12 10:00:06 -04:00
Adam Gastineau
610bcfa6d0
chore(mobile): add Cocoapods to mise (#30722)
* chore(mobile): add Cocoapods to mise

* Use new mise setup in CI
2026-08-12 17:23:14 +05:30
bo0tzz
0344c61e4c
feat: use version service for docs archive switcher (#30675) 2026-08-12 13:36:33 +02:00
Daniel Dietzler
199723261c
fix: owner cascade delete album (#30692) 2026-08-11 23:05:18 +02:00
bo0tzz
c7d7889ed0
fix: pass secrets to build-mobile (#29031) 2026-08-11 14:41:40 -04:00
Daniel Dietzler
a55dc80a56
chore: enforce strict equality checks (#30718) 2026-08-11 13:15:51 -04:00
Jason Rasmussen
00d10dab63
refactor: e2e user tests (#30716) 2026-08-11 12:39:25 -04:00
Jason Rasmussen
1b8cbaab97
refactor(server): migrate e2e auth/validation tests to controller specs (#30715)
Co-authored-by: Claude (thor) <claude@thor.local>
2026-08-11 11:02:14 -04:00
Jason Rasmussen
ee525f159b
refactor: queue in batches (#30698) 2026-08-11 10:01:42 -04:00
Daniel Dietzler
d1662fb2b6
fix: face label clipping (#30712) 2026-08-11 13:46:26 +00:00
Jason Rasmussen
927b4d0ea2
refactor: find or fail (#30697) 2026-08-11 09:27:35 -04:00
bo0tzz
9773d72428
chore: use gh release create instead of softprops action (#30577) 2026-08-11 14:55:27 +02:00
bo0tzz
cf17dcbfee
feat: derive mobile build number from semver (#30569) 2026-08-11 12:45:32 +00:00
bo0tzz
9273a4ca2a
chore: rework docker build workflow (#30626) 2026-08-11 14:40:12 +02:00
renovate[bot]
b4c27ee0b4
chore(deps): update github-actions (#30702) 2026-08-11 12:28:58 +02:00
renovate[bot]
427591fb9e
chore(deps): update grafana/grafana docker tag to v12.4.7-ubuntu (#30700) 2026-08-11 12:28:01 +02:00
renovate[bot]
23636382c4
chore(deps): update node.js to v24.19.0 (#30703) 2026-08-11 12:27:42 +02:00
renovate[bot]
c89104d144
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.8.3 (#30699) 2026-08-11 12:21:04 +02:00
renovate[bot]
7fea9f2066
fix(deps): update dependency @immich/ui to ^0.85.0 (#30705) 2026-08-11 12:02:06 +02:00
renovate[bot]
4c4170077c
fix(deps): update typescript-projects (#30539)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-08-11 11:39:38 +02:00
shenlong
675408b003
chore: migrate mobile translations accessor (5) (#30672)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-11 14:41:13 +05:30
shenlong
a8b3b30aba
chore: migrate mobile translations accessor (4) (#30671)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-11 14:41:13 +05:30
shenlong
d799698c51
chore: migrate mobile translations accessor (3) (#30670)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-11 14:41:12 +05:30
shenlong
835c35e940
chore: migrate mobile translations accessor (2) (#30668)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-11 14:41:12 +05:30
shenlong
565fbe4917
chore: migrate mobile translations accessor (1) (#30667)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-11 14:41:11 +05:30
Adam Gastineau
235daff561
chore(mobile): more complicated Freezed pass (#30452)
* chore(mobile): example freezed implementation on some models
2026-08-10 14:12:45 -07:00
Santo Shakil
0ff47f4178
fix(mobile): don't let a frozen sync block syncing on resume (#29870)
* fix(mobile): don't let a frozen sync block syncing on resume

* share the task lists between the cancel paths

* fix analyzer issues in resume integration test

* isolate the resume e2e test and pin the worker pool setup
2026-08-10 21:39:38 +06:00
Santo Shakil
9862e50aab
fix(mobile): decode remote thumbnails at displayed size (#29965)
* fix(mobile): decode remote thumbnails at displayed size

* use nullable decode size and reuse the tapped thumbnail in the viewer

* clean up decode size naming and guards
2026-08-10 21:14:23 +06:00
shenlong
c52bf9995b
chore: pump flutter to 3.44.9 (#30658)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-10 10:58:15 -04:00
Santo Shakil
35c2a90fca
fix(mobile): video playback controls dead for backed up videos opened from search (#30587)
* key video player state by asset id instead of hero tag

* expose id on BaseAsset instead of a separate playerKey
2026-08-09 23:54:55 +06:00
Priyanshu
2314330c78
fix: translation (#30549) 2026-08-08 18:39:14 +00:00
Matthew Momjian
161e15ee2f
fix(deployment): Add huggingface cache directory to Dockerfile (#30357)
* Add huggingface cache directory to Dockerfile

Updated Dockerfile to include huggingface cache directory.

* Update Dockerfile environment variables and comments

Removed MPLCONFIGDIR environment variable and adjusted comments.
2026-08-08 16:48:33 +00:00
shenlong
e9eafc3161
refactor: gallery permission notifier (#30477)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-08 11:14:14 -05:00
Mert
79e61c47c7
refactor(ml): remove insightface dependency (#30631)
* remove insightface dependency

* update tests
2026-08-08 12:07:30 -04:00
bo0tzz
8d086d61c4
chore: add good-first-issue llm rule to contributing.md (#30645) 2026-08-08 10:54:48 -05:00
shenlong
1c5770601d
chore: add asset debug action to all bottom sheets (#30611)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-08 10:05:30 +05:30
renovate[bot]
51c2f21497
chore(deps): update grafana/grafana:12.4.6-ubuntu docker digest to 124ddf0 (#30534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-07 10:32:26 -04:00
renovate[bot]
be5cc30f52
chore(deps): update prom/prometheus docker digest to 508729e (#30535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-07 10:28:24 -04:00
Daniel Dietzler
5ad1e4e0f7
fix: medium tests dependencies (#30612) 2026-08-07 11:36:20 +02:00
shenlong
a9a99cffbf
chore: cleanup translation dependency on i18n (#30596)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-06 09:40:32 -05:00
shenlong
6167618f5c
fix: asset stack stacked over asset details (#30598)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-06 19:29:21 +05:30
shenlong
63b90ec43d
fix: map not updating after viewing an asset (#30601)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-06 16:26:12 +05:30
bo0tzz
f361dbbb77
chore: clean up last mergify remnant (#30600) 2026-08-06 12:42:12 +02:00
shenlong
0687c0d3f7
fix: wire secondary action for bottom bar actions (#30583)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-06 01:15:31 +05:30
shenlong
4be8e1a9fc
fix: asset viewer bottom bar action colors (#30582)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-06 01:15:31 +05:30
Adam Gastineau
18d4a796bb
chore(mobile): simple freezed implementation on some models (#30168)
* chore(mobile): example freezed implementation on some models

* Don't commit Freezed generated code

* Freezed easy pass

* Formatting fix

* Generate Flutter debugging info
2026-08-05 12:00:49 -07:00
shenlong
f11d8b6a5a
fix(mobile): stop disabling androidx.startup initializers (#30559)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 23:59:01 +05:30
Santo Shakil
f0386ce089
fix(mobile): handle asset websocket events (#30499) 2026-08-06 00:15:21 +06:00
shenlong
db2033a4b0
chore: add reason for stopForegroundBackup in tests (#30573)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 22:45:50 +06:00
shenlong
555fbde840
fix: _safeRun is finally safe (#30571)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 21:22:31 +05:30
shenlong
1a40ef66b4
chore: log reason for foreground backup cancellation (#30570)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 20:18:27 +05:30
Santo Shakil
b2eb62dfa5
fix(mobile): keep backup remainder from going negative (#29011)
* recount the backup counters when a run starts

* clear the error before the recount

* create the cancel token before the recount
2026-08-05 20:17:05 +05:30
shenlong
1c7c28bb0d
chore: more riverpod lints from dcm (#30447)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 01:26:49 +05:30
shenlong
8450687db7
chore: riverpod lints from dcm (#30446)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-05 01:26:49 +05:30
Santo Shakil
00e813ba84
fix(mobile): stop long images squishing on ios (#29367)
* fix(mobile): stop long images squishing on ios

* fix(mobile): bound the original to 16384 with fast resize

* refactor(mobile): reuse the shared request options for the original

* fix(mobile): fix squished long image previews on ios

* fix(mobile): retry with exact resize when fast resize overshoots the texture bound

* fix(mobile): clamp preview targets to a minimum of one pixel

* fix nullable cast flagged by the stricter lints
2026-08-04 19:08:27 +00:00
renovate[bot]
099cb25ce0
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.8.1 (#30542) 2026-08-04 20:58:32 +02:00
Santo Shakil
8d2fdd6b8f
fix(mobile): stop websocket reconnect loop draining battery when server is unreachable (#29901)
* fix(mobile): stop websocket reconnect loop draining battery when server is unreachable

* fix(mobile): reconnect when the websocket is inactive and guard resume against pause

* move the test mocks to the service mocks file
2026-08-04 18:51:28 +00:00
Santo Shakil
d8e1ead6f4
fix(mobile): stale local renders after editing a photo on device (#30415)
* fix(mobile): key local image caches by content so on-device edits re-render

* use Object.hash and the local asset factory
2026-08-05 00:37:49 +06:00
Brandon Wees
cbd2d8a6bd
feat(widget): add toggle to match icon theme (#30428)
* feat(widget): add toggle to match icon theme

* chore: rename to forceFullColor

* fix: default setting
2026-08-04 12:05:59 -05:00
shenlong
29512081db
fix: use setRequireOriginal on SDK 29 and above (#29072)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-04 20:43:51 +05:30
Santo Shakil
7121e0a75c
fix(mobile): run one more sync round when a request arrives mid sync (#30478)
* fix(mobile): run one more sync round when a request arrives mid sync

* fix(mobile): only queue the extra sync round when the caller asks for it

* fix(mobile): skip the queued sync round when the sync fails

* clear the sync task in whenComplete and gate the queued round on the result

* simplify the queued flag gate
2026-08-04 14:59:19 +00:00
renovate[bot]
79d485c759
chore(deps): update github-actions (#30543) 2026-08-04 11:49:31 +02:00
shenlong
29e7ea5302
chore(web): use FUTO F-Droid repo in utilities (#30527)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-03 22:10:08 +02:00
Daniel Dietzler
0d7147dcec
fix: metadata extraction as LensModel can be a float (#30512) 2026-08-03 18:18:24 +02:00
bo0tzz
46c42e0935
chore: delete mergify config (#30521) 2026-08-03 16:50:28 +02:00
Santo Shakil
e5c3bdad17
fix(mobile): sync stack changes from the websocket (#30479) 2026-08-03 19:53:29 +05:30
shenlong
4d9a27691e
fix: action provider overrides (#30480)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-03 19:53:12 +05:30
Adrien Fabre
c2db36934f
feat: Display the number of selected items in AlbumPickerModal title (#30485) 2026-08-03 16:00:04 +02:00
Azharul Haque
774a9fd868
fix(mobile): correct mislabeled Bengali locale entry (#30519)
Fixes mislabeled locale entry: 'Bosnian (bl)': Locale('bn') actually loads Bengali, not Bosnian.
2026-08-03 13:30:15 +00:00
Santo Shakil
cee08a2320
fix(server): remove the asset row when an upload fails after creating it (#30349) 2026-08-03 14:59:05 +02:00
Daniel Dietzler
0894865352
chore: typescript 7 extension for vscode (#30516) 2026-08-03 13:38:09 +02:00
Gueye Papa Djadji
04453b7206
fix(server): reject invalid or deleted user when creating a partner (#30431) 2026-08-03 12:56:00 +02:00
Gueye Papa Djadji
da7d8c2e12
feat(web): search album description in add-to-album modal (#30462)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-08-03 12:42:44 +02:00
Lucas
4082fbf232
fix(web): Fix face thumbnail when swapping merge direction (#30466) 2026-08-03 12:28:18 +02:00
Ben Beckford
fb4a08c17a
chore(server): test assetFileFilter (#30430) 2026-08-03 11:55:40 +02:00
renovate[bot]
b3718fd18a
chore(deps): update dependency @testing-library/jest-dom to v7 (#30308) 2026-08-03 11:53:40 +02:00
bo0tzz
f1a90b7f36
feat: log hint about downgrades when migration is missing (#30493) 2026-08-03 11:06:38 +02:00
shenlong
cafd6c7c0f
refactor: cleanup actions & action tests (#30265)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:44 +05:30
shenlong
3d42c1424c
refactor: mobile upload action (#29949)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:43 +05:30
shenlong
bc84054ccc
refactor: mobile tag and download actions (#29948)
refactor: mobile tag and download action

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:43 +05:30
shenlong
1fa985babb
feat: album action (#29770)
refactor: mobile album actions

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:43 +05:30
shenlong
0a97b4b099
refactor: mobile share actions (#29947)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:42 +05:30
shenlong
9fcbb6eefc
refactor: mobile edit asset actions (#30264)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:42 +05:30
shenlong
a048e86217
refactor: mobile cast and slideshow action (#29768)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:42 +05:30
shenlong
36f12ec805
refactor: mobile open in browser, similar, set profile action (#29769)
refactor: mobile browser and similar photos action

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:41 +05:30
shenlong
3f3bc44258
refactor: mobile delete action (#29771)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:41 +05:30
shenlong
47d34e3b4b
refactor: mobile lock action (#29767)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:41 +05:30
shenlong
e6b5b0deb6
feat: archive action (#29362)
refactor: mobile archive action

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:40 +05:30
shenlong
208b7cf9ca
feat: stack action (#29370)
refactor: mobile stack action

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:40 +05:30
shenlong
5b0a324b68
feat: restore action (#29374)
* refactor: existing actions to new structure

# Conflicts:
#	mobile/lib/presentation/actions/action.dart
#	mobile/lib/presentation/actions/action.widget.dart
#	mobile/lib/presentation/actions/asset_debug.action.dart
#	mobile/lib/presentation/actions/favorite.action.dart
#	mobile/lib/presentation/actions/partner.action.dart
#	mobile/test/unit/presentation/actions/favorite_action_test.dart

* rename to actionitem

* review changes

* refactor: mobile restore action

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-08-01 08:06:40 +05:30
Adam Gastineau
483b375c26
fix(mobile): resolve owned assets when partner owns identical asset (#30137)
* fix(mobile): resolve owned assets when partner owns identical asset

* Add limit to auth user expressions

* Change debug method semantics
2026-07-31 14:37:12 +00:00
shenlong
dd8d068d44
chore: drop removed build_runner flag (#30427)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-31 06:14:33 -07:00
Adam Gastineau
920552c1c2
chore(mobile): depend on OpenAPI generation in appropriate codegen tasks (#30425) 2026-07-31 12:52:59 +00:00
Adam Gastineau
627b52fdaf
chore(mobile): remove last committed generated files (#30426) 2026-07-31 05:31:48 -07:00
renovate[bot]
7aa2068361
chore(deps): update dependency js-yaml to v5.2.2 [security] (#30441) 2026-07-31 13:47:57 +02:00
renovate[bot]
36cb86c886
fix(deps): update dependency js-yaml to v5 [security] (#30440)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-31 13:24:39 +02:00
renovate[bot]
8e0ad9b2c4
fix(deps): update typescript-projects (#30305)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-31 12:55:30 +02:00
shenlong
fe5c8ed0fb
refactor: base action (#29617)
* refactor: existing actions to new structure

# Conflicts:
#	mobile/lib/presentation/actions/action.widget.dart
#	mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart
#	mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart
#	mobile/test/unit/presentation/partner_page_test.dart

* rename to actionitem

* more cleanup

* review changes

* lint changes

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-30 17:02:14 -04:00
shenlong
405020eeee
chore: enable use_build_context_synchronously lint (#30367)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-30 20:10:36 +00:00
renovate[bot]
56ce7176b1
chore(deps): update dependency opentofu to v1.12.5 (#30299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-30 15:56:24 -04:00
Jason Rasmussen
0f4dddaf84
fix: migration order (#30424) 2026-07-30 15:55:21 -04:00
renovate[bot]
2fa07c0a42
chore(deps): update grafana/grafana docker tag to v12.4.6-ubuntu (#30304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-30 15:49:32 -04:00
renovate[bot]
aa565f5ca0
chore(deps): update github-actions (major) (#30309)
chore(deps): update github-actions

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-30 15:47:34 -04:00
Giacomo Pinato
6b6058c463
feat: store null instead of empty string for album.description (#30123)
Addresses the first column of issue #28832: album.description now stores
and returns null instead of an empty string.

Co-authored-by: Giacomo Pinato <giacomo.pinato@gmail.com>
2026-07-30 19:41:49 +00:00
Adam Gastineau
e7aace436d
chore(mobile): Apply stricter linting rules for correctness (#30372)
* chore(mobile): Apply stricter linting rules for correctness

* Added discarded_futures rule
2026-07-30 19:39:24 +00:00
Matthew Momjian
6b99f02232
fix(docs): Revise config file instructions and notes (#30418)
Revise config file instructions and notes

Updated the config file instructions to specify 'immich-config.json' and added notes about interaction with the web UI and microservices.
2026-07-30 15:37:35 -04:00
Matthew Momjian
7149dd8030
fix(docs): remove listing unraid as an "official" deployment (#30323)
* remove listing unraid as an "official" deploymeny

* mplconfig

* oops
2026-07-30 15:34:08 -04:00
Devesh Kolte
9732bebb55
fix(server): store null instead of empty string for user password (#30223) 2026-07-30 15:19:58 -04:00
bo0tzz
56fbca910e
chore: skip e2e tests when the stack fails to start (#30414) 2026-07-30 15:17:57 -04:00
Adam Gastineau
858aeadce8
chore(mobile): Apply stricter linting rules for Flutter and known issues (#30373) 2026-07-30 15:15:35 -04:00
Adam Gastineau
9a143f0047
chore(mobile): remove Pigeon generated code (#30343) 2026-07-30 15:14:50 -04:00
bo0tzz
4a68a87531
fix: use trixie-slim base image for cli and e2e-auth-server (#30411)
The alpine suffix part wasn't getting updated by renovate, and the 3.20
chain stopped getting node updates too. This also just aligns the base
image distro with what we use elsewere.
2026-07-30 15:14:18 -04:00
bo0tzz
7c44c29a8f
chore: deflake album to asset backfill sync test (#30417) 2026-07-30 15:13:44 -04:00
bo0tzz
6e1e79585e
chore: install mise tools from the lockfile in the server image (#30416) 2026-07-30 15:13:12 -04:00
bo0tzz
6afcc39fb2
chore: sequence pnpm installs in mise tasks (#30412) 2026-07-30 15:12:42 -04:00
bo0tzz
a316ba35ca
fix: shared check for server setup availability (#30311)
* fix: shared check for server setup availability

* chore: add medium test

* feat: require @Authenticated decorator everywhere

* fix: lints
2026-07-30 15:12:25 -04:00
bo0tzz
1f16fe16c2
chore: enable merge queue batching (#30410)
This waits for up to 2 min to group up to 3 PRs into the same draft. That should cut down on the amount of draft PRs that get created. The main downsides are the 2 minute wait when only queueing one PR, and less failure isolation: on an error, mergify has to do bisect runs to figure out which PR failed the batch, which takes more time than purely sequential operation. All of this can be tuned further of course.
2026-07-30 15:58:08 +00:00
shenlong
7e70f90c15
fix: apply disabled styling to UI buttons (#30322)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-30 09:23:08 +00:00
Ben Beckford
9d03a92b10
fix: assetFileFilter path matching (#30394) 2026-07-30 11:08:24 +02:00
Adam Gastineau
d864a90811
chore(mobile): Apply stricter linting rules for formatting (#30370)
* chore(mobile): Apply stricter linting rules for formatting

* Formatting fixes
2026-07-30 08:56:46 +00:00
Alexander J
0293414abd
fix: minor typos (#30385) 2026-07-30 00:05:11 +02:00
bo0tzz
4988c0a4c2
chore: update merge queue configuration (#30381)
Signed-off-by: null <null>
2026-07-29 20:39:37 +00:00
Adam Gastineau
6c01b4f5d8
chore(mobile): remove OpenAPI generated code (#30287)
* chore(mobile): remove OpenAPI generated code

* Don't reference generated files for CI checks
2026-07-29 20:33:30 +00:00
Adam Gastineau
27a29b6fab
chore(mobile): remove Drift generated code (#30297)
* chore(mobile): remove Drift generated code

* Remove Drift test generated files from tree

* Make static_analysis use the shared mobile:codegen action

* Remove static_analysis generated file diffing
2026-07-29 20:29:07 +00:00
Santo Shakil
534b8746e4
fix(mobile): refresh person thumbnail when the featured photo changes (#29350) 2026-07-29 22:20:26 +05:30
bo0tzz
af5ff4983a
chore: skip release label validation on merge queue PRs (#30361) 2026-07-29 15:08:35 +00:00
bo0tzz
3ab09352eb
feat: add spam rule to CONTRIBUTING.md genAI section (#30355) 2026-07-29 14:17:49 +00:00
Jason Rasmussen
04baedcffb
fix: privacy policy link (#30344) 2026-07-28 21:12:10 -04:00
Jason Rasmussen
bcf6e66e26
chore: remove old makefiles (#30339) 2026-07-28 13:49:26 -04:00
Matthew Momjian
1094b69946
fix(deployment): matplotlib in rootless deployments (#30328) 2026-07-28 10:57:21 -04:00
Daniel Dietzler
8cb5bf92c2
chore: clarify contributing guidelines (#30330) 2026-07-28 15:55:17 +02:00
bo0tzz
b468459b1a
chore: add test for admin controller permissions (#30316) 2026-07-28 15:44:51 +02:00
Daniel Dietzler
4609adbb6d
feat: add cantonese for mobile (#30318) 2026-07-28 11:54:23 +00:00
renovate[bot]
e3ecb08711
chore(deps): update github-actions (#30302) 2026-07-28 13:21:59 +02:00
renovate[bot]
fbf4535469
chore(deps): update docker.io/valkey/valkey:9 docker digest to 3acc068 (#30298) 2026-07-28 12:25:37 +02:00
Daniel Dietzler
f68815e35f
fix: calendar heatmap api key permissions (#30314) 2026-07-28 12:16:59 +02:00
renovate[bot]
4225281f9f
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.7.15 (#30301) 2026-07-28 11:36:43 +02:00
Daniel Dietzler
6c86d39f6d
fix: calendar heatmap permissions (#30310) 2026-07-28 11:33:10 +02:00
Alex
cf213c81e3
fix: make sure iOS memory widget render (#30172)
* fix: make sure iOS memory widget render

* pr feedback
2026-07-27 16:37:26 -05:00
github-actions
8aa95c6747 chore: version v3.1.0 2026-07-27 18:20:33 +00:00
Weblate (bot)
e5310e2d2c
chore(web): update translations (#29781)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/af/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/az/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de_CH/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gsw/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ka/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/kn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ms/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ne/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: 100daysummer <bobbydochev@gmail.com>
Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: AgentTricky <andreweditz26@gmail.com>
Co-authored-by: Aggår <aggaar1@proton.me>
Co-authored-by: Ahmed Khaleel Shihab <ahmed91shihab@gmail.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alexander <email.alexander@yahoo.com>
Co-authored-by: Alice Visek <alice@weblate.org>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: Arseniy <67965122+ArsenijN@users.noreply.github.com>
Co-authored-by: Arthur Eduardo Good <arthuregood@protonmail.com>
Co-authored-by: Azharul Haque <haque@azharul.com>
Co-authored-by: Carlo Lecce <cclecce@hotmail.it>
Co-authored-by: Crystal <crystal@iw2tryhard.dev>
Co-authored-by: Derik Nel <derik.nel@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: George Hallai <georgehallai@outlook.com>
Co-authored-by: Guy Slutzker <0606guy@gmail.com>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Hans Cats <hanscats@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Haru Ijima <haruijimakun@gmail.com>
Co-authored-by: Hosted Weblate user 157829 <taylorbobaylor@gmail.com>
Co-authored-by: Hugo Dantas <hugodantasjpa@gmail.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jakub Biernat <kubziom.pr@gmail.com>
Co-authored-by: Jarukrit Sripaploen <jarukritsripaploen@protonmail.com>
Co-authored-by: Jaspreet Singh <j.singh@maersk.com>
Co-authored-by: Javid Yagubali <javid.yagubali@gmail.com>
Co-authored-by: Jayden Lo <jaydenlo08@gmail.com>
Co-authored-by: Jeppe Nellemann <jepnel@proton.me>
Co-authored-by: John Molkavitch <jblum66@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Klenner Martins Barros <klenne.al@gmail.com>
Co-authored-by: Linas Samuchovas <linsamuchovas@gmail.com>
Co-authored-by: Lucka <Samzhao0727@gmail.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Max <max.uden@t-online.de>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: Melih Ozkan <malihozkan156@gmail.com>
Co-authored-by: Mona Lisa <monalisa@users.noreply.hosted.weblate.org>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: NOBOIKE <yazeedalhimali@gmail.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Nikola Todorov <nickybg87@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: Runskrift <anders@rimfrost.nu>
Co-authored-by: SKAL <sir_kalot@yahoo.it>
Co-authored-by: Samipya Mainali <mainalisamipya@gmail.com>
Co-authored-by: Securitocat <dodongchien1232@gmail.com>
Co-authored-by: Skanda <skillwiz94@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Syahmi Azhar <prsyahmi@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: TV Box <realceday.tvbox@gmail.com>
Co-authored-by: Tal Gruper <tal.gruper@gmail.com>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: Tiago <tiagoj@ik.me>
Co-authored-by: Tim <timvanbergen01@gmail.com>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Tyson Igana <ta1.20081102@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Yuko <hi+github@yuko.im>
Co-authored-by: Yusuf Arda <yusufarda7813@gmail.com>
Co-authored-by: ZaKyatto <jevan.badloe10@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: chamdim <chamdim@protonmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: emrothenberg <eitanrothenberg@duck.com>
Co-authored-by: ezn24 <eznotsimple@gmail.com>
Co-authored-by: fennek <thil.hafner@gmail.com>
Co-authored-by: gilles <raynaud.g@gmail.com>
Co-authored-by: matthew <matthewstevenallen@protonmail.com>
Co-authored-by: noob translator <nzbnoob.overdress150@passinbox.com>
Co-authored-by: oxygenxc <github.n36px@aleeas.com>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: reducedradius <137701630+flytothehighest@users.noreply.github.com>
Co-authored-by: rubes <mail@armd.one>
Co-authored-by: shamgamer <shamgrt1234@gmail.com>
Co-authored-by: shawshuai <weblate@sshaw.cn>
Co-authored-by: thedmo <benjamin@minjaben.ch>
Co-authored-by: thomaLV <thomas.l.villanger@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: wlusr <wlusr@users.noreply.hosted.weblate.org>
Co-authored-by: 蘭蘭露 <flandrescarlettw@gmail.com>
Co-authored-by: 정동걸 <i.jdk.dev@gmail.com>
2026-07-27 18:17:47 +00:00
Adam Gastineau
e6f8256b25
fix(mobile): prevent timeline scroll to top on unrelated pages (#30281) 2026-07-27 10:29:46 -05:00
shenlong
04a38ba91c
refactor: asset update method (#30201)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-27 10:15:36 -04:00
Jason Rasmussen
bc6bf388c0
fix(web): single grid row spacing (#30277) 2026-07-27 14:13:11 +00:00
Adam Gastineau
e3385ce183
chore(mobile): harden mobile OpenAPI codegen and dependency install (#30202)
* chore(mobile): harden mobile OpenAPI codegen and dependency install

* Cleaner mise cascade

* Version openapi-generator-cli with mise

* Relocate tool definition

* Update lock file
2026-07-27 06:07:31 -07:00
bo0tzz
3606144190
chore: clarify MediaRepository extract logging (#30249) 2026-07-26 13:02:57 -04:00
Daniel Dietzler
409734e1db
fix: shared by user detail panel (#30187) 2026-07-24 15:44:37 -04:00
Daniel Dietzler
b08f6b9c50
chore: medium tests system config helpers (#30174) 2026-07-24 21:27:31 +02:00
shenlong
7b023d9a71
chore: pump flutter to 3.44.8 (#30205)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-24 18:32:13 +00:00
Adam Gastineau
9abe72aced
fix(mobile): align iOS widget text box with widget's corners (#30176)
* fix(mobile): align iOS widget text box with widget's corners

* Improve image centering and fix formatting

* Remove duplicate preview
2026-07-24 23:34:27 +05:30
shenlong
829b4e748e
feat: button longpress handler (#30200)
* feat: button longpress handler

* review suggestion

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-24 23:34:21 +05:30
bo0tzz
4a5f13d0e5
fix: don't skip person thumbnail generation if ML is disabled (#30194) 2026-07-24 10:06:32 -04:00
NOBOIKE
5fa920a5f0
fix(web): use RTL transform origin in AdaptiveImage (#30182) 2026-07-24 10:44:33 +02:00
Jason Rasmussen
acc7e6b299
fix: min faces user preference (#30177) 2026-07-23 17:06:09 -04:00
Adam Gastineau
3af94a4805
fix(mobile): properly align iOS widget loading state (#30173) 2026-07-23 12:31:33 -07:00
Adam Gastineau
f488a28018
chore(mobile): force swift-structured-queries lock to 0.34.0 (#30166) 2026-07-23 17:04:01 +00:00
NOBOIKE
29605f710e
fix(web): mirror onboarding navigation icons in RTL (#30158)
Co-authored-by: root <root@yazeed.localdomain>
2026-07-23 17:23:36 +02:00
Yaros
792d88961a
feat(mobile): custom date range for map (#26205)
* feat(mobile): custom date range for map

* refactor: rename timerange & remove isvalid

* refactor: rename customtimerange variables

* refactor: add back setRelativeTime

* refactor: implement suggestions

* refactor: suggestions

* fix: ifPresent

* fix: context.locale

* chore: restrict selection

* refactor: move options to mapconfig

* refactor: move model to domain

* chore: locale toLanguageTag

* add map codec tests

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-23 20:48:15 +05:30
shenlong
20ae07e228
fix: run background tasks in root isolate (#30101)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-23 20:06:37 +05:30
Adam Gastineau
a0a0aa3f3c
fix(mobile): allow URL validation to pass when scheme is not provided (#30142)
* fix(mobile): allow URL validation to pass when scheme is not provided

* Do normalize and validate in the same step
2026-07-23 19:46:48 +05:30
upmcplanetracker
ceef4037f1
fix: hypertext link to example docker-compose.rootless.yml (#30155) 2026-07-23 15:16:44 +02:00
NOBOIKE
cbb565b7b7
fix(web): mirror asset viewer navigation icons in RTL (#30151) 2026-07-23 12:22:45 +02:00
okxint
3bd580e37d
fix(web): restore correct back route when opening person asset via direct URL (#30129) 2026-07-23 09:06:59 +00:00
Timon
f24a128319
feat(server): new search schemas and query builders (#28686) 2026-07-22 22:33:23 +00:00
Santo Shakil
36dfc98527
fix(mobile): birthday picker date order follows locale (#29419)
* fix(mobile): birthday picker date order follows locale

* test(mobile): cover date order locales for the birthday picker
2026-07-23 02:32:17 +05:30
shenlong
9403e71d23
refactor: add asset update method (#29384)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-22 19:51:06 +00:00
shenlong
1f81eac8ab
refactor: toast repository (#29386)
refactor: feedback repository

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-22 19:48:28 +00:00
Pavel Miniutka
564cda5088
chore(mobile): Adds Belarusian language option in settings on mobile (#29939)
chore(mobile): add missing Belarusian (be) language option in settings
2026-07-22 17:53:21 +00:00
Alex
a7f1d495c6
fix: wrong corner radius of recently added link (#30140) 2026-07-22 16:44:14 +00:00
Jason Rasmussen
b9f6c4aaf2
feat: password invalidate sessions (#30125) 2026-07-22 12:09:05 -04:00
shenlong
23778551f7
fix: locked view and asset view provider (#30136)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-22 09:51:33 -05:00
bo0tzz
457da17c5d
fix: always set extension from provided file (#29839)
* fix: always set extension from provided file

The livephoto case can also happen with other files; for example, we
receive a DNG file as JPEG but then still call it .dng

Fixes #29805, partially addresses #28602

* chore: test cases for file extension naming
2026-07-22 18:29:54 +05:30
Jan Bader
12f741d911
chore(cli): remove -h shorthand for --skip-hash (#30119)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-22 13:25:29 +02:00
Aditya Raj Singh
a0c7b81142
fix(server): dissolve stack when its non-primary assets are deleted (#29354) 2026-07-22 13:00:52 +02:00
Daniel Dietzler
20997823d8
chore: mise monorepo support is now stable (#30131) 2026-07-22 12:55:47 +02:00
okxint
8a875f5978
fix(web): use correct date field for shift-click range in Recently Added (#30071) 2026-07-22 11:53:45 +02:00
renovate[bot]
8435d219e4
chore(deps): update dependency opentofu to v1.12.4 (#28491) 2026-07-22 09:30:08 +00:00
Ben Beckford
8bbc252703
feat: exif metadata workflow filter (#29295) 2026-07-22 11:20:57 +02:00
renovate[bot]
2d3cf8ffaa
chore(deps): update dependency terragrunt to v1.1.1 (#29399) 2026-07-22 11:16:09 +02:00
renovate[bot]
33cba2f9af
chore(deps): update grafana/grafana docker tag to v12.4.5-ubuntu (#29670) 2026-07-22 10:59:29 +02:00
renovate[bot]
75053ebfba
chore(deps): update terraform cloudflare to v4.52.8 (#29671) 2026-07-22 10:59:04 +02:00
Yaros
ba9ec7c80e
fix: backup delay translation key parsing (#30112) 2026-07-22 10:58:38 +02:00
Yaros
99370b2ac4
fix: admin user details responsive layout (#30114) 2026-07-22 10:54:26 +02:00
Ben Beckford
0f5bf6071f
chore(server): test assetDateFilter workflow method (#30110) 2026-07-22 10:48:31 +02:00
Ijas
998fd5f6a8
fix(mobile): send date-only value for memories query param (#30049)
* fix(ios-widget): send date-only value for memories query param

Server v3.0.3 (immich-app#29907) validates the /memories `for` query
param with a date-only schema (YYYY-MM-DD). The iOS widget still sent a
full ISO 8601 datetime, so every request 400s and the widget shows
"Unable to connect to your Immich instance" on any server >= v3.0.3.

Build the value from the local calendar day, matching what the web
client (memory-manager.svelte.ts) and Android already send, instead of
a UTC datetime.

Also fix a related bug in the timeline provider: fetchMemory was
called with a bare try inside a Task, so a failure silently dropped
the error and never called completion, leaving the widget in a stale
state instead of showing an error card. Wrap it in a guard/try?, same
pattern already used in getSnapshot.

Fixes immich-app#30044

Signed-off-by: ajuijas <189517297+ajuijas@users.noreply.github.com>

* address review: drop unrelated widget change and comment

Signed-off-by: ajuijas <189517297+ajuijas@users.noreply.github.com>

---------

Signed-off-by: ajuijas <189517297+ajuijas@users.noreply.github.com>
Co-authored-by: ajuijas <189517297+ajuijas@users.noreply.github.com>
Co-authored-by: Santo Shakil <santoshakil@users.noreply.github.com>
2026-07-22 14:19:19 +06:00
renovate[bot]
f413c446e3
chore(deps): update github-actions (#30089) 2026-07-22 09:15:38 +02:00
Yaros
c9539a4aa8
fix(mobile): map unresponsive after viewing asset (#27036)
* fix(mobile): map unresponsive after viewing asset

* chore: better fix
2026-07-22 02:56:23 +05:30
Imperator Ruscal
a560e08693
fix: re-evaluate OIDC role claim on every login and support array values (#29991)
* fix(server): re-evaluate OIDC role claim on every login and support array values

Previously the OIDC role claim (immich_role) was only read at user
auto-registration time and only accepted as a scalar string, so admin
status never updated after the first login and array-valued role/group
claims (common with Keycloak, Entra ID, etc.) were silently ignored.

Now the role claim is normalized from either a string or an array of
strings, and existing users have their isAdmin flag synced from the
claim on every login, keeping the IdP as the source of truth for
privileges while leaving isAdmin untouched when the claim is blank.

* fix(server): use .includes() instead of .some() for role claim check

Satisfies unicorn/prefer-includes lint rule flagged by CI.

* fix(server): resolve missing role claim to standard user and fix test mock

Default the OIDC role claim to 'user' when the IdP omits it so
auto-registration doesn't crash, and add the missing getAdmin mock in the
auth.service test so it correctly simulates an existing admin rather than
the first-user-registration path.

* fix: claim sync logic

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2026-07-21 17:21:19 -04:00
Yaros
49c0baf076
fix: disable slideshow crossfade on reduced motion (#29826)
* fix: disable slideshow crossfade on reduced motion

* chore: share photoview instances

* chore: listen to changes
2026-07-22 01:59:26 +05:30
shenlong
3485dbffda
refactor(mobile): extract CachedKeyValueRepository (#28789)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-21 20:23:50 +00:00
Yaros
e508781125
fix: limit ocr overlay to images (#30116) 2026-07-21 20:23:04 +00:00
Peter Ombodi
a30ebab70c
fix(mobile): refresh asset stack after deleting viewer item (#28164)
* fix(mobile): refresh asset stack after deleting viewer item

* fix(mobile): clamp stack index,
filter visible stack assets

* refactor(mobile): simplify stack asset refresh handling

* fix(mobile): revert changes in DeleteLocalActionButton

* style(mobile): reformat code

* fix(mobile): remove redundant setStack

---------

Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com>
2026-07-22 01:48:47 +05:30
Daniel Dietzler
5d7283e44d
fix: album update event emitting (#30120) 2026-07-21 19:43:27 +00:00
Lorenzo Rota
73329a8ce8
fix(server): send id_token_hint on OIDC logout (#29720)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 19:19:13 +02:00
Santo Shakil
d5adfb97dd
fix(mobile): treat wired ethernet as unmetered on ios (#29351)
check the metered flags instead of the interface type, so ethernet counts like android already does.
2026-07-21 11:02:36 -05:00
Priyanshu
b70cb58bc8
fix(web): improve slideshow controls visibility on bright backgrounds (#29950)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-21 15:58:51 +00:00
Santo Shakil
943bfafb7a
fix(mobile): show real error when an asset can't be added to an album (#29754)
* fix(mobile): show real error when an asset can't be added to an album

* fix(mobile): show added toast when an add-to-album partly succeeds

* docs(mobile): explain why a partial add-to-album shows added
2026-07-21 10:56:50 -05:00
Timon
ee4bd3f833
feat: add album asset event handling (#29008)
* feat: add album asset event handling

* fix medium tests

* patch medium test

* collapse in AlbumUpdate
2026-07-21 10:55:46 -05:00
Adam Gastineau
7a7303aceb
fix(mobile): properly group download tasks for Live Photos (#29952)
* fix(mobile): properly group download tasks for Live Photos

* Remove completely unused onLivePhotoDownloadStatus

* Remove dead code
2026-07-21 10:45:15 -05:00
Adam Gastineau
0a7c067b20
chore(mobile): bump maplibre to 0.26.2 in SwiftPM lock (#30108) 2026-07-21 10:44:30 -05:00
Luis Nachtigall
c8de56c422
fix(mobile): prevent duplicate album creation during submission (#30003)
* fix: prevent duplicate album creation during submission

* Nit fix

---------

Co-authored-by: Adam Gastineau <adam@agg.im>
2026-07-21 15:31:23 +00:00
shenlong
0e4572c37b
chore: pin photo_manager to 3.9.0 (#30091)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-21 10:06:27 -05:00
Santo Shakil
14a2776453
fix(mobile): add album picker to the partner view bottom sheet (#30099) 2026-07-21 10:05:34 -05:00
renovate[bot]
59bc81423c
chore(deps): update github-actions (major) (#30095) 2026-07-21 16:37:38 +02:00
Roberto Carlos Martinez Arriaga
e6fff3b15e
feat(web): add maintenance link to command palette (#30016) 2026-07-21 16:27:23 +02:00
renovate[bot]
755df23b22
chore(deps): update ghcr.io/devcontainers/features/docker-in-docker docker tag to v4 (#30094) 2026-07-21 16:05:54 +02:00
Jason Rasmussen
899f547053
fix(web): lens model search (#30088) 2026-07-21 13:54:28 +00:00
Santo Shakil
4938fd4c89
fix(mobile): back up files moved into a watched folder on android (#29872) 2026-07-21 19:09:50 +05:30
Santo Shakil
e81a79224c
fix(mobile): stop double close animation when dismissing an asset (#29413) 2026-07-21 19:08:45 +05:30
shenlong
7b0f58b2c1
refactor: gracefully stop during engine startup (#29179)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-21 19:08:13 +05:30
renovate[bot]
ae8398ffe4
chore(deps): update docker.io/valkey/valkey:9 docker digest to 8e8d64b (#30085) 2026-07-21 15:36:01 +02:00
renovate[bot]
aa08dad1f5
chore(deps): update dependency eslint-plugin-unicorn to v72 (#30092)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 15:33:46 +02:00
renovate[bot]
ce022233ae
chore(deps): update base-image to v202607211135 (#30090) 2026-07-21 13:58:02 +02:00
Rust
1b4d41324e
fix(server): file uploads for files with extension only filenames (#30024)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 11:57:28 +00:00
renovate[bot]
8365cdd59e
chore(deps): update dependency typescript to v7 (#29903)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 13:36:57 +02:00
Mees Frensel
1cec7021e8
fix(web): space between device type and version (#30102) 2026-07-21 11:08:29 +00:00
renovate[bot]
1b0f473457
fix(deps): update typescript-projects (#30087)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-21 11:01:23 +00:00
renovate[bot]
e918658cc1
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.7.11 (#30086) 2026-07-21 12:24:32 +02:00
Nikola Nedevski
2e587fc7e8
feat(docs): add bulgarian readme (#29427)
* Added Bulgarian readme.md

* Updated collaborators link for BG readme collaborators section.
2026-07-21 00:10:08 -04:00
renovate[bot]
df970da59e
chore(deps): update dependency eslint-plugin-unicorn to v70 - abandoned (#29684)
* chore(deps): update dependency eslint-plugin-unicorn to v70

* fix: linting

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-20 23:47:14 -04:00
Rust
8061a2e5ff
fix(web): fix Country/State filters when set to Unknown (#30026)
* fix(web): create emptyStringToNull helper func and apply it to country/state search payload generation

* chore: feedback

* fix: other empty string fields

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2026-07-20 21:15:00 -05:00
Daniel Dietzler
77091b0107
fix: search statistics locked folder permissions (#30063)
fix; search statistics locked folder permissions
2026-07-20 20:08:42 -04:00
Daniel Dietzler
4c754f2999
fix: timebuckets locked permissions (#30066) 2026-07-20 20:07:56 -04:00
Timon
6fa3f2feac
chore: bump openapi-generator version to v7.24.0 (#30067)
* remove patch in favor of [OpenAPITools/openapi-generator#23930](https://github.com/OpenAPITools/openapi-generator/issues/23930)

partially revert "chore(mobile): make openapi requests abortable (#28692)"

* gen client

* bump openapi-generator version to v7.24.0

* gen client

* enum migration changes

* drop enum exhaustion switch check
2026-07-20 20:06:23 -04:00
Adam Gastineau
3f6897ef80
chore(mobile): enable Xcode displaying issues from Flutter inline (#30080) 2026-07-20 14:16:12 -05:00
Matthias Keim
89d0a9d59f
fix(web): attach file picker input to DOM so iOS Safari fires change … (#29660)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-20 19:03:42 +00:00
Adam Gastineau
45a6ea84af
fix(mobile): prevent crash on video widget dispose (#30078) 2026-07-20 19:00:37 +00:00
shenlong
bf64f3867b
fix: do not show the whats new page on fresh login (#30072)
* fix: do not show the whats new page on fresh login

* review suggestion

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-20 12:24:01 -05:00
Priyanshu
4a4d468aa2
fix(web): refresh folder view after asset deletion (#29899) 2026-07-20 18:20:52 +02:00
Priyanshu
522def1ed6
fix(web): align ContextMenu z-index with design-system token (#30015)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-20 13:54:59 +00:00
Timon
3adc3920fb
chore: bump openapi-generator version to v7.23.0 (#28916) 2026-07-20 15:06:51 +02:00
Matthew Momjian
00cb50cc67
fix(docs): remove ref to synology channel (#30051)
synology channel
2026-07-19 21:22:17 -05:00
Alex
f2b0b696f6
fix: long press share quality override preference settings (#30030) 2026-07-18 12:07:18 -05:00
Luis Nachtigall
b5401eb120
fix(mobile): update album creation to use user-defined name from dialog (#30002)
fix: update album creation to use user-defined name from dialog
2026-07-17 23:45:20 +05:30
Ben Beckford
12fc8bac18
feat: workflow filter assets by upload path (#30000)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-17 09:46:48 +00:00
Ben Beckford
f2c00c107d
fix(server): return workflow steps in ascending order (#29999) 2026-07-17 11:30:40 +02:00
shenlong
f19f30ec66
fix: reset override for kebab buttons (#29380)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-17 01:19:55 +05:30
shenlong
297316c7b8
refactor: owned action assets filter (#29382)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-16 19:49:30 +00:00
renovate[bot]
f18065b3c2
chore(deps): update dependency @types/node to ^24.13.3 (#29892) 2026-07-16 20:31:45 +02:00
renovate[bot]
a3dd19cd2f
fix(deps): update typescript-projects (#29633)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-16 20:14:05 +02:00
shenlong
9cea3f2375
chore: pump drift (#29113)
* chore: pump drift

* run codegen

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-16 17:32:30 +00:00
shenlong
507448d797
chore: pump home_widget (#29981)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-16 21:58:11 +05:30
Yaros
fe3eb7d865
feat(web): undo archive from toast (#27061)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-16 17:53:02 +02:00
Adam Gastineau
90dc328c22
chore(mobile): update maplibre_gl to 0.26.2 (#29772) 2026-07-16 07:47:48 -07:00
shenlong
a5623c41ec
chore: pump flutter to 3.44.6 (#29886)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-16 20:17:03 +05:30
Adam Gastineau
3a016f4451
chore(mobile): drop support for iOS 14 (#29780)
chore(mobile): drop support for iOS 14 in advance of iOS and Xcode 27
2026-07-16 07:45:22 -07:00
Adam Gastineau
0af8284456
chore(mobile): expose signing and bundle identities through envvars and config (#29077)
* chore(mobile): expose signing and bundle identities through envvars and config

* Move instructions to now shared docs/developer/setup.md
2026-07-16 07:44:50 -07:00
renovate[bot]
ad7a9bd189
chore(deps): update prom/prometheus docker digest to 3c42b89 (#29667) 2026-07-16 14:01:41 +02:00
Ryan Rockey
80cdfebbc7
fix: zero byte image uploads (#29426) 2026-07-16 13:00:07 +02:00
Diogo Correia
c50a807bc1
feat(web): add wake lock when uploading assets (#29820) 2026-07-16 12:41:37 +02:00
bo0tzz
26e07548a7
chore: add GHSA as contact in security.txt (#29940) 2026-07-16 12:30:44 +02:00
cevdet
f73c53c563
chore(seo): remove redundant twitter metadata tags (#29801) 2026-07-16 12:29:38 +02:00
Mert
d2744f52b1
fix(server): disable heif security limit (#29954)
disable security limit
2026-07-16 04:40:33 -05:00
Yuxin Wang
d8ed7d7bb9
feat(cli): Add --visibility flag to immich CLI upload subcommand (#29614) 2026-07-15 14:11:03 -05:00
renovate[bot]
834cc54abc
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.7.7 (#29893) 2026-07-15 18:45:35 +00:00
Daniel Dietzler
eda605c534
fix(web): clear birth date (#29959) 2026-07-15 18:32:53 +00:00
Daniel Dietzler
b75e6520bb
chore: mobile code checks for i18n changes (#29958) 2026-07-15 20:20:42 +02:00
Timon
4c2d2284aa
refactor(server): album user route params (#29503) 2026-07-15 18:15:41 +00:00
Daniel Dietzler
5d29f3edc1
fix: mobile translations (#29955) 2026-07-15 18:02:29 +00:00
Mees Frensel
e00dcf7850
fix(web): URI encode slug and reduce confusion for users (#29796)
Co-authored-by: bwees <brandonwees@gmail.com>
2026-07-15 19:32:05 +02:00
Andreas Heinz
e7f773197f
fix(server/workflow): add trigger for external libraries AssetCreate (#29597) 2026-07-15 19:30:33 +02:00
bo0tzz
2237078bd5
chore: bump create-workflow-token action to v3 (#29571) 2026-07-15 14:33:23 +02:00
Jorge Montejo
55dc579059
feat(web): Keep show more open on duplicates (#29734)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-15 11:22:36 +00:00
Raul
b4530b0671
fix(server): workflow date filter, make end date inclusive (#29876)
Co-authored-by: Raul Plesa <raul.plesa@external.grifols.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-07-15 10:50:03 +00:00
Lorenzo Montanari
a04e2ddeca
fix(server): updated default CSP config to support videos from V3 player (#29830) 2026-07-15 12:49:18 +02:00
Kobi Hikri
5f2e8a4fcf
chore(security.txt): bump expired Expires field (RFC 9116) (#29932) 2026-07-15 10:19:44 +00:00
Priyanshu
dc55b234d9
fix(web): use Container component for responsive admin maintenance la… (#29917)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-15 12:18:55 +02:00
Priyanshu
f6bf22d9ca
fix(web): hide stack thumbnail tray in slideshow mode (#29918)
Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-15 12:10:35 +02:00
Santo Shakil
2cdbd0d00f
fix(mobile): apply exif orientation to remote raw photos on android (#29906) 2026-07-14 19:10:41 -04:00
github-actions
cd308ad930 chore: version v3.0.3 2026-07-14 21:10:19 +00:00
Alex
12061f3bf8
fix: Ken Burns effect on sliver app bar jittery on Android (#29890) 2026-07-14 19:21:33 +00:00
Daniel Dietzler
2460d431af
feat: system integrity docs (#29911) 2026-07-14 14:00:27 -05:00
Matthew Momjian
24556cd3fc
fix(docs): update fdroid link (#29797)
Co-authored-by: Brandon Wees <brandonwees@gmail.com>
2026-07-14 20:04:59 +02:00
Adam Gastineau
c84ab54889
fix(mobile): clear zoom state when swapping assets (#29909) 2026-07-14 12:18:18 -05:00
renovate[bot]
19e59ebd98
chore(deps): update github-actions (#29894) 2026-07-14 18:44:32 +02:00
Mert
89b0d906e8
fix(server): set hvc1 tag when using hwa (#29908)
fix hvc1 check
2026-07-14 11:03:30 -04:00
Daniel Dietzler
3c43e8d6c1
fix: revert "fix(mobile): send a full datetime from the android memories widget" (#29913) 2026-07-14 14:41:43 +00:00
Daniel Dietzler
2ed8b2bddd
fix: memory search date validation (#29907) 2026-07-14 16:34:38 +02:00
Santo Shakil
9c8d718ddc
fix(mobile): send a full datetime from the android memories widget (#29689) 2026-07-14 09:25:06 -05:00
renovate[bot]
20e123c839
chore(deps): update base-image to v202607132058 (#29895) 2026-07-14 11:16:42 +02:00
Daniel Dietzler
35fcca6254
fix: locked visibility in search random endpoint (#29887) 2026-07-13 20:03:20 +00:00
shenlong
ffafc144c6
refactor: use single bucket for none grouping (#29880)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-13 15:01:59 -05:00
bo0tzz
89d31aaba1
feat: publish app releases to FUTO F-Droid repo (#29804)
* feat: publish app releases to FUTO F-Droid repo

* chore: separate parse step
2026-07-13 14:57:40 -05:00
Alex
f2ddace584
fix: system appbar icon color has incorrect color in asset viewer (#29885) 2026-07-13 14:50:38 -05:00
shenlong
99883096d6
fix: still part of live photo hidden on bg upload (#29884)
* fix: still part of live photo hidden on bg upload

* add foreground tests

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-13 14:50:06 -05:00
Daniel Dietzler
84dff19ca9
fix: unauthorized album owner update (#29883) 2026-07-13 11:46:18 -05:00
Priyanshu
da8774801b
fix(mobile): correct person age singular pluralization (#29868)
The ICU plural string for person_age_years in i18n/en.json only defined
the other plural category, so the intl MessageFormat runtime always
fell back to it — displaying '1 years old' instead of '1 year old'.

Added the missing one plural category:

Before: {years, plural, other {# years}} old
After:  {years, plural, one {# year} other {# years}} old

No Dart source changes required. formatAge already uses the
localization system correctly.

Fixes #29865

Co-authored-by: priyanshuANDcoad <priyanshu23154050@akgec.ac.in>
2026-07-13 14:49:06 +00:00
Ben Beckford
9057ae9759
fix(web): closing plugin method picker selects "unknown" method (#29784) 2026-07-13 09:41:28 -05:00
Alex
557189d7a8
chore: add Adam to mobile's codeowner (#29816) 2026-07-10 15:10:52 -05:00
Mees Frensel
19313e75fd
chore(web): fix testing deprecations (#29799) 2026-07-10 14:30:12 +02:00
Weblate (bot)
352c8086f9
chore(web): update translations (#29774)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ml/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aiixi Bear <bearshen1019@outlook.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alaksandr Łopart <laporte0723@gmail.com>
Co-authored-by: Alexis Rossfelder <rossfelderalexis@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Biepa <github@flippie.de>
Co-authored-by: Branko Kokanovic <branko@kokanovic.org>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Dusan Hlavaty <dhlavaty@gmail.com>
Co-authored-by: Emil Warelius <emilwarelius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: Grzegorz Leks <grzegorz.leks@gmail.com>
Co-authored-by: Gyutaek Kang <gtkang2000@naver.com>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Hosted Weblate user 157208 <benjamin3.hanke@gmail.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Huy Trieu <thetop1102@gmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jesus Barba <jesusbarba11@gmail.com>
Co-authored-by: JiZPaper <JiZPaper@gmail.com>
Co-authored-by: Jins K Joy <mailjinskjoy@gmail.com>
Co-authored-by: Jonas Franke <90050350+JonasFranke@users.noreply.github.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Ketan Avhad <ketaavha@gmail.com>
Co-authored-by: Kevin wang <jiqi963@gmail.com>
Co-authored-by: Klenner Martins Barros <klenne.al@gmail.com>
Co-authored-by: Liviu Roman <contact@liviuroman.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Marc Portabella Navarro <portabella.marc@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Matteo De Carli <matteo.de.carli01@gmail.com>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: MrKoby07 <hirmerkorbinian122@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nico Sendan <nico@sendan.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Ohne Sonne <ohne_sonne_1312@protonmail.com>
Co-authored-by: Okie <leopetrovic11@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: PiTrick <dev@pitrick.link>
Co-authored-by: Rubber Ducky <rd1945@gmail.com>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Simone Ognibene <ognibene2001@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: TV Box <realceday.tvbox@gmail.com>
Co-authored-by: TheDevIsHere <bagigio2005@gmail.com>
Co-authored-by: Tiago <tiagojoaquimsamsung@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Tom Golden <tmgldn@users.noreply.hosted.weblate.org>
Co-authored-by: Tomislav Renić <trenic@gmail.com>
Co-authored-by: Tushar Harsora <tusharharsora95@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vallabh Sharma <srivallabhsharma@gmail.com>
Co-authored-by: Yusuf Arda <yusufarda7813@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bonjur <ivanpercsity08@gmail.com>
Co-authored-by: canxunwangluo <canxunwangluo@163.com>
Co-authored-by: kylo32 <kylo32@gmail.com>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: pluwen chen <pluwen@gmail.com>
Co-authored-by: puv <dev@puv.pm>
Co-authored-by: rubes <mail@armd.one>
Co-authored-by: vatahome <vataszilamer@pm.me>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2026-07-09 20:01:50 +00:00
github-actions
443b856ac3 chore: version v3.0.2 2026-07-09 17:33:21 +00:00
Weblate (bot)
d18edfed34
chore(web): update translations (#29415)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ml/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aiixi Bear <bearshen1019@outlook.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alaksandr Łopart <laporte0723@gmail.com>
Co-authored-by: Alexis Rossfelder <rossfelderalexis@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Biepa <github@flippie.de>
Co-authored-by: Branko Kokanovic <branko@kokanovic.org>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Dusan Hlavaty <dhlavaty@gmail.com>
Co-authored-by: Emil Warelius <emilwarelius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: Grzegorz Leks <grzegorz.leks@gmail.com>
Co-authored-by: Gyutaek Kang <gtkang2000@naver.com>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Hosted Weblate user 157208 <benjamin3.hanke@gmail.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Huy Trieu <thetop1102@gmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jesus Barba <jesusbarba11@gmail.com>
Co-authored-by: JiZPaper <JiZPaper@gmail.com>
Co-authored-by: Jins K Joy <mailjinskjoy@gmail.com>
Co-authored-by: Jonas Franke <90050350+JonasFranke@users.noreply.github.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Ketan Avhad <ketaavha@gmail.com>
Co-authored-by: Kevin wang <jiqi963@gmail.com>
Co-authored-by: Klenner Martins Barros <klenne.al@gmail.com>
Co-authored-by: Liviu Roman <contact@liviuroman.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Marc Portabella Navarro <portabella.marc@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Matteo De Carli <matteo.de.carli01@gmail.com>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: MrKoby07 <hirmerkorbinian122@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nico Sendan <nico@sendan.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Ohne Sonne <ohne_sonne_1312@protonmail.com>
Co-authored-by: Okie <leopetrovic11@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: PiTrick <dev@pitrick.link>
Co-authored-by: Rubber Ducky <rd1945@gmail.com>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Simone Ognibene <ognibene2001@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: TV Box <realceday.tvbox@gmail.com>
Co-authored-by: TheDevIsHere <bagigio2005@gmail.com>
Co-authored-by: Tiago <tiagojoaquimsamsung@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Tom Golden <tmgldn@users.noreply.hosted.weblate.org>
Co-authored-by: Tomislav Renić <trenic@gmail.com>
Co-authored-by: Tushar Harsora <tusharharsora95@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vallabh Sharma <srivallabhsharma@gmail.com>
Co-authored-by: Yusuf Arda <yusufarda7813@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bonjur <ivanpercsity08@gmail.com>
Co-authored-by: canxunwangluo <canxunwangluo@163.com>
Co-authored-by: kylo32 <kylo32@gmail.com>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: pluwen chen <pluwen@gmail.com>
Co-authored-by: puv <dev@puv.pm>
Co-authored-by: rubes <mail@armd.one>
Co-authored-by: vatahome <vataszilamer@pm.me>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2026-07-09 17:30:32 +00:00
bo0tzz
f6d6dbc3c7
chore: remove pgvecto.rs from docs footnote (#29729)
* chore: remove pgvecto.rs from docs footnote

* Update docs/docs/install/environment-variables.md

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>

---------

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>
2026-07-09 11:36:26 -05:00
shenlong
4fe33289b8
fix: ignore log db corruptions (#29656)
* fix: ignore log db corruptions

* review suggestion

* log corruption recovery

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-09 11:36:04 -05:00
Mees Frensel
2db1e02cdf
fix(web): always initialize language manager to set lang and dir (#29696) 2026-07-09 13:40:15 +00:00
Daniel Dietzler
fea0db02f3
fix: emit album update event when uploading assets through shared link (#29757) 2026-07-09 15:21:16 +02:00
Mees Frensel
4fd58885e2
fix(web): recursively unnest class arrays (#29755) 2026-07-09 12:39:12 +02:00
Ben Beckford
c5222c7579
chore(server): use bounded numbers in workflows (#29009)
* chore(server): use bounded numbers in workflows

* chore: increase precision for lat/lon filtering

* chore: re-add integer to a json schema type
2026-07-08 17:15:48 -07:00
Santo Shakil
c4f084f4a5
fix(mobile): play videos from search results and folder view (#29693)
* fix(mobile): play videos from search results and folder view

* pr suggestion

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-08 20:22:35 +00:00
Daniel Dietzler
66e30d5fa1
fix: missing files integrity checks (#29737) 2026-07-08 14:53:46 -05:00
Daniel Dietzler
053a52aec8
fix: iso date parsing for three digit years (#29739) 2026-07-08 14:53:16 -05:00
Santo Shakil
2ad554fc61
fix(mobile): update timeline args on layout constraint changes (#29653)
* fix(mobile): update timeline args on layout constraint changes

* chore(mobile): temporary blank timeline diagnostics

* chore(mobile): error logs when the timeline goes blank

* chore(mobile): remove blank timeline debug logging

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-08 14:13:32 -05:00
Kha Nguyen
427321a102
fix(web): clear asset selection when search context changes (#29738) 2026-07-08 14:08:39 -05:00
Daniel Dietzler
655cd01d57
fix: search metadata for album shared links (#29742) 2026-07-08 20:32:20 +02:00
Mert
cfd95f0196
fix(server): stream ffprobe packets (#29723) 2026-07-08 12:51:22 -04:00
renovate[bot]
439ace9dba
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.7.2 (#29678) 2026-07-08 13:22:06 +02:00
Ben Beckford
5c2fb2245a
feat(server): date workflow filter (#29040) 2026-07-08 12:05:44 +02:00
shenlong
924e6251fd
fix: do not log out on automatic url switch (#29715)
fix: sync headers to external endpoints

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-07 21:21:37 -05:00
Mert
3aebb82905
fix(mobile): http request handling after isolate death (#29714)
* use ports for objc

* add task reaper

* cleanup, fewer allocations

* link to pr
2026-07-07 16:31:10 -04:00
Daniel Dietzler
7a9b9e01be
feat: user agnostic database restore (#29512) 2026-07-07 21:16:48 +02:00
Daniel Dietzler
0cbe07c4d4
fix: integrity checks dto validation (#29621) 2026-07-07 20:49:26 +02:00
renovate[bot]
45ea248e8b
chore(deps): update github-actions (#29677) 2026-07-07 19:56:57 +02:00
Adam Gastineau
8cbf0db3b3
fix(mobile): disable broken Xcode queue debugging (#29657) 2026-07-07 12:26:19 -05:00
Adam Gastineau
a225c962de
fix(mobile): properly handle live photos locally (#29462)
* fix(mobile): properly handle live photos locally

* Added basic motion photo data tests

* Cover mismatched platforms and motion photos

* Revert unnecessary storage.repository changes

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-07 12:25:43 -05:00
Mees Frensel
daabab8c1c
fix(docs): update developer setup (#29661) 2026-07-07 17:07:35 +00:00
Brandon Wees
a4f28dafb2
fix(mobile): missing crop options and rotation scaling (#29645)
* fix: missing aspect ratio options in mobile editor

* fix: smooth rotation

* chore: comments

* chore: better preset management and tooling

* lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-07 12:05:50 -05:00
Daniel Dietzler
704b287bba
fix: integrity checks checksum pagination (#29516) 2026-07-07 18:42:14 +02:00
Ben Beckford
c7b4a798cb
fix: incorrect grouping of recently added assets (#29666)
* fix: incorrect grouping of recently added assets

* chore(web): lint

* chore(web): refactor timeline utc to local dates

* chore(mobile): index remote asset upload dates

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-07 11:21:58 -05:00
Daniel Dietzler
f8e78792b8
fix: oauth account linking (#29624) 2026-07-07 11:21:27 -05:00
shenlong
0ebb723b39
fix: wrap migrations in transaction (#29664)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-07 10:54:24 -05:00
Adam Gastineau
99a8354145
fix(mobile): match iOS scroll to top and tabbar scroll (#28469) (#29705)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-07-07 10:53:35 -05:00
shenlong
7ee2f3644f
fix(mobile): mark Live Photo motion part hidden on upload (#29699)
* fix(mobile): mark Live Photo motion part hidden on upload

* review suggestion

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-07 09:27:39 -05:00
bo0tzz
d2a46ade78
chore: webhook headers ordering (#29695) 2026-07-07 12:09:57 +00:00
Mert
68b04e8e8f
chore: simplify hls description (#29680)
simplify hls description
2026-07-07 07:45:40 +00:00
Santo Shakil
58b1c5e8fb
fix(mobile): fix 10-bit heic and avif colors on android (#29631)
* fix(mobile): fix 10-bit heic and avif colors on android

* fix(mobile): share the decode pool across flutter engines

* vectorize conversion

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2026-07-06 21:16:09 +00:00
Matthew Momjian
6f45521610
fix(docs): clarify version policy (#29654)
* version policy

* Update docs/docs/install/upgrading.md

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-06 19:07:31 +00:00
Mert
69d568f289
feat(server): hls variant configuration (#29613)
* configure variants

* openapi

* dynamic level

* unnecessary width/height fields

* add web config

* allow 1440p and 2160p

* more helpful descriptions

* simplify level picker

* formatting

* integer instead of number
2026-07-06 14:57:48 -04:00
Daniel Dietzler
a6f0253544
chore: improve added to album messages (#29641) 2026-07-06 17:21:49 +02:00
Jins K Joy
32352fa375
feat(docs): add Malayalam language support in README files (#29593) 2026-07-06 17:15:00 +02:00
Tom B
6aa5651032
fix(docs): use correct heading name for DB dump (#29483)
match docs to actual UI copy
2026-07-06 10:13:22 -05:00
Daniel Dietzler
4e25db2989
fix: chinese browser locales recognition (#29622) 2026-07-06 09:42:47 -05:00
Daniel Dietzler
4f5e0d3505
chore: clearer album users dropdown (#29629) 2026-07-06 09:40:26 -05:00
Alex
b3abe0bb5d
chore: keep selection state after sharing action (#29640) 2026-07-06 14:40:18 +00:00
renovate[bot]
c04acc8951
chore(deps): update base-image to v202607061334 (#29634) 2026-07-06 16:01:22 +02:00
renovate[bot]
af32a8ca7c
chore(deps): update github-actions (#29400) 2026-07-06 15:25:52 +02:00
renovate[bot]
ed83fb3f5a
chore(deps): update ghcr.io/jdx/mise docker tag to v2026.6.14 (#29632) 2026-07-06 14:54:02 +02:00
renovate[bot]
628de417e9
chore(deps): lock file maintenance (mise) (#29637) 2026-07-06 14:52:48 +02:00
renovate[bot]
be02c00afd
chore(deps): lock file maintenance (mise) (#29636) 2026-07-06 14:52:42 +02:00
Wonsuk Choi
43abb9a92f
fix(web): wrap hardcoded strings with i18n translation function (#29612)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-06 09:57:04 +00:00
TJ Horner
103585ceb9
fix(web): add ARIA label to ServerStatisticsCard (#29497)
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-07-06 10:35:47 +02:00
Daniel Dietzler
6da890ee37
fix: web search timeline visibility (#29518) 2026-07-03 17:41:25 +02:00
Lukas Schaefer
e7ba8d3632
fix: live transcoding when viewing from a link share (#29471) 2026-07-03 11:34:37 -04:00
Lukas Schaefer
5645149e09
fix: downmix audio to stereo to fix broken channel layout (#29485)
* fix: downmix audio to stereo to fix broken channel layout

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>

* add todo

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>

---------

Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-07-03 10:55:12 -04:00
Alex
5a104f9dbe
chore: update docs with v3 tags (#29312) 2026-07-03 12:27:18 +02:00
bo0tzz
13d17b3a11
fix: re-add gCastEnabled negation (#29499) 2026-07-03 11:05:22 +02:00
github-actions
f77c8a4699 chore: version v3.0.1 2026-07-02 19:24:13 +00:00
Daniel Dietzler
bf4b020856
fix: setting input field description spacing for description snippet (#29433) 2026-07-02 14:06:03 -05:00
shenlong
f73796597c
fix: albums broken on v2 mobile clients (#29446)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-02 14:04:37 -05:00
Ben Beckford
237734bb26
feat(web): recently added link in sidebar (#29039)
* feat(web): recently added link in sidebar

* chore(mobile): update openapi patches
2026-07-01 23:12:50 +00:00
Weblate (bot)
4b54fef82e
chore(web): update translations (#29410)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translation: Immich/immich

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Charles IdB <charles.issert2braux@gmail.com>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Harsh Kevadia <kevadiyaharsh@gmail.com>
Co-authored-by: Hosted Weblate user 156232 <53017937+parol100@users.noreply.github.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jeppe Nellemann <jepnel@proton.me>
Co-authored-by: Mahmoud Dwidar <modydodo2055@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: Yago Raña Gayoso <yago.rana.gayoso@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: ikeno-web <ikeno@nextcore-consulting.com>
Co-authored-by: rubes <mail@armd.one>
2026-07-01 11:05:25 +00:00
Daniel Dietzler
0050332391
fix: e2e version test (#29412) 2026-07-01 12:41:45 +02:00
github-actions
05d838b560 chore: version v3.0.0 2026-06-30 20:03:47 +00:00
Weblate (bot)
82b70c1ab6
chore(web): update translations (#29347)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sq/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translation: Immich/immich

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Charles IdB <charles.issert2braux@gmail.com>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Harsh Kevadia <kevadiyaharsh@gmail.com>
Co-authored-by: Hosted Weblate user 156232 <53017937+parol100@users.noreply.github.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jeppe Nellemann <jepnel@proton.me>
Co-authored-by: Mahmoud Dwidar <modydodo2055@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: Yago Raña Gayoso <yago.rana.gayoso@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: ikeno-web <ikeno@nextcore-consulting.com>
Co-authored-by: rubes <mail@armd.one>
2026-06-30 20:01:21 +00:00
Daniel Dietzler
02506424a7
feat: integrity checks admin settings (#29406) 2026-06-30 14:58:51 -05:00
Ben Beckford
6a7a34d294
chore: make webhooks workflow-agnostic (#29404) 2026-06-30 15:27:30 -04:00
Alex
165bca4b0a
feat: new feature message (#29388)
* feat: new feature board

* wip

* wip

* wip

* lint

* lint

* pr feedback

* pr feedback

* i18n

* i18n
2026-06-30 13:53:13 -05:00
Brandon Wees
d4b994301f
fix: version compatability check (#29405) 2026-06-30 18:44:53 +00:00
Daniel Dietzler
deeb042a9e
feat: honor album access permissions in search endpoints (#29352) 2026-06-29 22:27:22 +02:00
Daniel Dietzler
b4cc406a3f
fix!: search endpoints visibility can be omitted (#29385) 2026-06-29 22:00:02 +02:00
Padhai_Kaneer
df383c1ead
fix(server): face region coordinates parsing (#29333) 2026-06-29 17:05:20 +02:00
Santo Shakil
af2efda310
fix(mobile): apply exif orientation to android raw photos (#29337) 2026-06-29 10:59:09 -04:00
shenlong
5164feb5b4
chore: disable logs in tests (#29360)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-29 09:43:51 -04:00
shenlong
56eb69b328
chore: cleanup unit tests (#29357)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-29 09:38:00 -04:00
github-actions
ac74bca18b chore: version v3.0.0-rc.4 2026-06-27 13:14:47 +00:00
shenlong
e9d1951858
fix: dispatch menu onPressed manually (#29353)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-27 08:10:47 -05:00
Santo Shakil
6e1143e799
fix(mobile): hide video thumbnail when video is ready (#29012) 2026-06-26 22:20:40 -05:00
github-actions
09d4a6815f chore: version v3.0.0-rc.3 2026-06-26 16:49:02 +00:00
Weblate (bot)
23d1dbcb23
chore(web): update translations (#29290)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Abel Mislovic <mislovica@gmail.com>
Co-authored-by: Adjenz <adjenz@proton.me>
Co-authored-by: Andrea <ghosts.adm@gmail.com>
Co-authored-by: Conrad Menz <weblate@spamstopover.de>
Co-authored-by: Dan Bishop <d@nbishop.uk>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Dinesh R <dinodinu@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: KZeliop <kzeliop@gmail.com>
Co-authored-by: Kong <cptkong1@gmail.com>
Co-authored-by: Maj Mohar <maj.mohar4@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Oleksandr Yurov <oyurov@icloud.com>
Co-authored-by: Sean Young <assanges@users.noreply.hosted.weblate.org>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vincent Halman <vincent_halman@hotmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2026-06-26 16:46:30 +00:00
Jason Rasmussen
6507b1f94c
feat: pump doc references (#29331) 2026-06-26 16:45:06 +00:00
Santo Shakil
29949bebe4
fix(mobile): only toggle backup from the switch, not the whole row (#29236)
tapping anywhere on the enable backup row flipped backup on or off, so it was easy to toggle by accident. now only the switch does it.
2026-06-26 20:00:08 +05:30
Daniel Dietzler
d85e599ad9
feat: ultimate plugin type safety (#29340) 2026-06-26 16:27:19 +02:00
jameskimmel
b16cc496b2
docs: MS smtp guide (#29289)
Signed-off-by: jameskimmel <17176225+jameskimmel@users.noreply.github.com>
2026-06-26 16:16:38 +02:00
Ben Beckford
953ef5c047
feat: webhook workflow action (#29258)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-26 16:08:45 +02:00
jullang
a876d4a9f1
fix: small typo in openapi-spec (#29308)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-26 10:28:52 +00:00
Daniel Dietzler
688241a462
feat: plugin-sdk safety all around (#29323) 2026-06-25 18:23:55 -04:00
shenlong
cb1af3a8ec
feat: favorite bottom sheet action (#29320)
* chore: cleanup partner action test

* feat: favorite bottom sheet action

* review suggestions

* implicit favorite handling

* feat: viewer favorite icon to action (#29321)

* feat: viewer favorite icon to action

* feat: advance info action

* implicit favorite handling

* feat: viewer favorite icon to action

# Conflicts:
#	mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

* chore: timeline action test (#29324)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

* clear selection only on success

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-25 16:55:06 -04:00
shenlong
49a821b0d0
chore: fix mobile test flakiness (#29325)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-25 22:56:59 +05:30
shenlong
3a7034d25e
chore: cleanup partner action test (#29296)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-25 10:49:34 -04:00
Yaros
4099fa6b4a
fix(mobile): app doesn't exit full-screen mode (#29301)
* fix(mobile): app doesn't exit full-screen mode

* chore: rename restoreSystemUI to restoreEdgeToEdge
2026-06-24 20:48:01 -05:00
Daniel Dietzler
9751530af8
feat: plugin wrapper type safety (#29300) 2026-06-24 15:22:35 -04:00
Daniel Dietzler
0931a19c5c
fix: run test suite for plugin changes (#29311) 2026-06-24 16:29:46 +00:00
Alex
08b2e2c0b5
fix(docs): Revert v3 bump (#29310)
Revert "fix(docsc): v3 bump (#29246)"

This reverts commit dc7d57ff9a.
2026-06-24 11:18:37 -05:00
Santo Shakil
e5b50a55a4
fix(mobile): blank notifications page after enabling notifications (#29232)
the old notification toggles were removed in a cleanup, so once notifications were enabled the page had nothing left and went blank. show a "notifications enabled" status tile with a shortcut to the system notification settings instead.
2026-06-24 09:15:28 -05:00
shenlong
9d6c219276
fix: current viewer asset reactivity (#29282)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 18:40:24 +00:00
shenlong
f29f86542c
feat: partner actions (#29281)
* feat: partner actions

# Conflicts:
#	i18n/en.json

* cleanup

* fix tests

* ci fix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 23:50:59 +05:30
okxint
5165cf1d2f
fix(mobile): force AssetViewerPage recreation on repeated view intents (#29235)
* fix(mobile): force AssetViewerPage recreation on repeated view intents

When View in Immich is triggered a second time while the viewer is
already open, auto_route's replaceAll reuses the existing route (same
type, null key) and Flutter keeps the old ConsumerState alive. The
PageController and preloader inside _AssetViewerState are late final,
so they never reset — the viewer stays frozen on the previous asset.

Passing UniqueKey() to AssetViewerRoute ensures each view intent
creates a fresh widget element, so initState runs, the PageController
is initialised from scratch, and the new TimelineService from the
updated ProviderScope override is picked up correctly.

Fixes #29230

* clean up

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2026-06-23 17:43:56 +00:00
shenlong
f4c8459484
feat: mobile actions (#29280)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 16:50:57 +00:00
Mees Frensel
22ec449e43
chore: remove unused i18n strings (#29288) 2026-06-23 18:02:29 +02:00
Weblate (bot)
0b1019c344
chore(web): update translations (#29204)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de_CH/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fil/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gsw/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ne/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ur/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translation: Immich/immich

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Benjamin Kunz <benjamin.kunz.ch@gmail.com>
Co-authored-by: Cohinem <twitch9ofe@gmail.com>
Co-authored-by: Conrad Menz <weblate@spamstopover.de>
Co-authored-by: Damian Krysta <damian@krysta.dev>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Edmundas <edmius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Insoo Seok <tocoyo@gmail.com>
Co-authored-by: Jayden Lo <jaydenlo08@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: Melih Ozkan <malihozkan156@gmail.com>
Co-authored-by: Mr.Biswas <kallan.biswas001@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nicolas Ceballos <nicoshafes@gmail.com>
Co-authored-by: Para <ahh-produktivitet@tutanota.com>
Co-authored-by: Piero B. <biagini93@gmail.com>
Co-authored-by: PierreLapolla <plapolla9@gmail.com>
Co-authored-by: Ronnel <misc.woe@outlook.ph>
Co-authored-by: Sakib Iqbal <sakib.iqbal@gmail.com>
Co-authored-by: Saugat Tripathi <saugat.tripathi76@gmail.com>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Umair Jibran <wablate@umairjibran.com>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vitor Coelho <vitorc195@gmail.com>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: muysup <79565421+MuySup@users.noreply.github.com>
Co-authored-by: Òscar Casajuana <elboletaire@underave.net>
2026-06-23 15:50:30 +00:00
Mees Frensel
06f3b4f259
refactor(web): simple actions (#29257) 2026-06-23 17:08:46 +02:00
Ben Beckford
99f94a363d
chore(web): workflow property ordering (#29261)
* chore(web): workflow property ordering

* chore(web): extract schema property sorting to method
2026-06-23 13:03:33 +00:00
Daniel Dietzler
c3092b1c2c
chore: basque was missing on mobile (#29284) 2026-06-23 08:57:05 -04:00
renovate[bot]
0656e7e231
chore(deps): update dependency typescript to v6 (#28772)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-23 14:01:09 +02:00
renovate[bot]
1692b81b7c
chore(deps): lock file maintenance (pub) (#28733) 2026-06-23 12:43:35 +02:00
renovate[bot]
ff2028c4c8
chore(deps): update prom/prometheus docker digest to a75c5a3 (#29271) 2026-06-23 12:43:16 +02:00
Timon
f22836e1bf
refactor(server): describe check upload id as string (#29274) 2026-06-23 12:42:42 +02:00
renovate[bot]
7dd02ffbad
chore(deps): update github-actions (#29272)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2026-06-23 08:49:21 +00:00
shenlong
e51c4cb355
feat: column button (#29265)
* refactor: icon buttons implicit loading

* chore: cleanup

* feat: ui color override

* feat: column button

* feat: ui menu item (#29266)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 20:56:00 -05:00
shenlong
d4102c0489
refactor: ui icon buttons implicit loading (#29263)
* refactor: icon buttons implicit loading

* chore: cleanup

* feat: ui color override (#29264)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 16:24:52 -04:00
shenlong
30a73c1105
feat: mobile-ui snackbar (#29260)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 16:22:26 -04:00
Jason Rasmussen
ec7c0f9ec8
fix: sync backfill (#29267) 2026-06-22 16:19:01 -04:00
Brandon Wees
a5198e23a8
refactor: use SemVer classes for version compatability message (#29056)
* refactor: use SemVer classes for version compatability message

* chore: readd major version compatabilty messages

* fix: remove 1.106.0 check

(we dont support v1 servers anymore)
2026-06-22 11:28:56 -04:00
Mees Frensel
51f2905fcc
fix(web): remove map's fullscreen button (#29192) 2026-06-22 16:58:07 +02:00
Vogeluff
3b7d75c18a
feat(web): Add text-white-shadow to elements and increase the shadows effect (#29165)
* fix(web): increase text shadow strength for white text on thumbnails

* fix(web): fix class order for text-white-shadow

* fixup: format fix
2026-06-22 09:43:15 -05:00
Daniel Dietzler
c484bd99b6
fix: ignore external libraries for integrity report checksum check (#29248) 2026-06-22 13:56:24 +00:00
Anthony Clerici
c0bf5a4c56
fix(server): use VBR for QSV so the max bitrate is respected (#29240)
* fix(server): use VBR for QSV so the max bitrate is respected

* update test
2026-06-22 09:56:20 -04:00
MuySup
d9d50d2848
fix: turkish readme translation (#29234)
* Translation completed

3-2-1 rule translated

* Fix formatting of warning message in Turkish README
2026-06-22 09:55:58 -04:00
Daniel Dietzler
c7453a67fd
fix: detail panel people reactivity and iterator consumption (#29250) 2026-06-22 15:47:09 +02:00
Daniel Dietzler
e918e3a313
feat: keyboard seeking for new video player (#29208) 2026-06-22 09:42:59 -04:00
Matthew Momjian
dc7d57ff9a
fix(docsc): v3 bump (#29246)
v3 bump in docs
2026-06-21 20:44:58 -05:00
Alex
b24a617142
chore: bump mobile build (#29215) 2026-06-19 12:50:20 -05:00
Mees Frensel
62b00a1f26
refactor: slideshow and setalbumcover actions (#29211)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-19 13:54:33 +00:00
Timon
95fc5e9682
docs: clarify duplicate exif merging intent (#29203) 2026-06-19 10:57:35 +02:00
1870 changed files with 51273 additions and 385412 deletions

View file

@ -83,7 +83,7 @@
}
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"ghcr.io/devcontainers/features/docker-in-docker:4": {
// https://github.com/devcontainers/features/issues/1466
"moby": false
}

14
.gitattributes vendored
View file

@ -1,29 +1,15 @@
mobile/openapi/**/*.md -diff -merge
mobile/openapi/**/*.md linguist-generated=true
mobile/openapi/**/*.dart -diff -merge
mobile/openapi/**/*.dart linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
mobile/android/**/*.g.kt -diff -merge
mobile/android/**/*.g.kt linguist-generated=true
mobile/ios/**/*.g.swift -diff -merge
mobile/ios/**/*.g.swift linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
mobile/lib/infrastructure/repositories/db.repository.steps.dart -diff -merge
mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generated=true
mobile/test/drift/main/generated/** -diff -merge
mobile/test/drift/main/generated/** linguist-generated=true
packages/sdk/fetch-client.ts -diff -merge
packages/sdk/fetch-client.ts linguist-generated=true

View file

@ -15,7 +15,7 @@ jobs:
outputs:
uses_template: ${{ steps.check.outputs.uses_template }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: .github/pull_request_template.md
sparse-checkout-cone-mode: false

View file

@ -12,6 +12,10 @@ on:
default: 'development'
type: string
secrets:
PUSH_O_MATIC_APP_CLIENT_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
KEY_JKS:
required: true
ALIAS:
@ -51,14 +55,15 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@ -79,19 +84,21 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./mobile
@ -103,14 +110,14 @@ jobs:
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: 'zulu'
java-version: '17'
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.gradle/caches
@ -124,16 +131,11 @@ jobs:
with:
packages: ''
- name: Get Packages
working-directory: ./mobile
run: flutter pub get
- name: Install dependencies
run: mise //mobile:install:ci
- name: Generate translation file
run: mise //mobile:codegen:translation
- name: Generate platform APIs
run: mise //mobile:codegen:pigeon
working-directory: ./mobile
- name: Perform codegen
run: mise //mobile:codegen
- name: Build Android App Bundle
working-directory: ./mobile
@ -181,7 +183,7 @@ jobs:
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: github.ref == 'refs/heads/main'
with:
path: |
@ -198,55 +200,47 @@ jobs:
contents: read
# Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: macos-15
runs-on: macos-26
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./mobile
- name: Install Flutter dependencies
working-directory: ./mobile
run: flutter pub get
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Generate platform APIs
run: mise //mobile:codegen:pigeon
- name: Resolve iOS Swift Packages
working-directory: ./mobile
run: flutter build ios --config-only --no-codesign
- name: Setup Ruby
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: ./mobile/ios
- name: Install CocoaPods dependencies
working-directory: ./mobile/ios
run: |
pod install
- name: Install dependencies
run: mise //mobile:install:ci
- name: Perform codegen
run: mise //mobile:codegen
- name: Resolve iOS Swift Packages
working-directory: ./mobile
run: flutter build ios --config-only --no-codesign
- name: Create API Key
env:

View file

@ -19,13 +19,15 @@ jobs:
actions: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-actions: write
- name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}

View file

@ -20,16 +20,17 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@3530478ec30f84adedbfeb28f0d9527a290f50a9 # v0.0.57
uses: oasdiff/oasdiff-action/breaking@b7c3adeb54330db1903d27c61db520e5661ad55b # v0.1.11
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
fail-on: ERR
review: false
check-mobile-patches:
runs-on: ubuntu-latest
@ -37,19 +38,18 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ github.token }}
working_directory: ./mobile
- name: Get packages
working-directory: ./mobile
run: flutter pub get
- name: Install dependencies
run: mise //mobile:install:ci
- name: Fetch base spec from main
run: |

View file

@ -31,19 +31,20 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -63,25 +64,26 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
@ -96,7 +98,7 @@ jobs:
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
flavor: |
latest=false
@ -107,7 +109,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
- name: Build and push image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64

View file

@ -39,7 +39,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: ghcr.io/immich-app/mdq:main@sha256:2a64c7f045cb7b580fbdf3614d7d1805f5775fec453e3d1023764180efa8c70b
image: ghcr.io/immich-app/mdq:main@sha256:74b93dfbf13f8ee73eb35d8d5195ff667f93f9bc55df5ae453dd43a461c821fa
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:

View file

@ -44,20 +44,21 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -70,7 +71,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -83,6 +84,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
category: '/language:${{matrix.language}}'

View file

@ -14,6 +14,10 @@ concurrency:
permissions: {}
env:
GHCR_NAMESPACE: ghcr.io/${{ github.repository_owner }}
DOCKERHUB_NAMESPACE: docker.io/altran1502
jobs:
pre-job:
runs-on: ubuntu-latest
@ -23,14 +27,15 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@ -43,8 +48,6 @@ jobs:
- 'machine-learning/**'
force-filters: |
- '.github/workflows/docker.yml'
- '.github/workflows/multi-runner-build.yml'
- '.github/actions/image-build'
force-events: 'workflow_dispatch,release'
retag_ml:
@ -57,10 +60,33 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
include: &ml-variants
- image: immich-machine-learning
device: cpu
- image: immich-machine-learning
device: cuda
suffixes: '-cuda'
platforms: linux/amd64
- image: immich-machine-learning
device: openvino
suffixes: '-openvino'
platforms: linux/amd64
- image: immich-machine-learning
device: armnn
suffixes: '-armnn'
platforms: linux/arm64
- image: immich-machine-learning
device: rknn
suffixes: '-rknn'
platforms: linux/arm64
- image: immich-machine-learning
device: rocm
suffixes: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}'
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -69,10 +95,10 @@ jobs:
- name: Re-tag image
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
REPOSITORY: ${{ github.repository_owner }}/${{ matrix.image }}
TAG_OLD: main${{ matrix.suffixes }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffixes }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffixes }}
run: |
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
@ -85,12 +111,9 @@ jobs:
packages: write
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest
strategy:
matrix:
suffix: ['']
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -100,9 +123,9 @@ jobs:
env:
REGISTRY_NAME: 'ghcr.io'
REPOSITORY: ${{ github.repository_owner }}/immich-server
TAG_OLD: main${{ matrix.suffix }}
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
TAG_OLD: main
TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}
TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}
run: |
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
@ -114,40 +137,27 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- device: cpu
- device: cuda
suffixes: '-cuda'
platforms: linux/amd64
- device: openvino
suffixes: '-openvino'
platforms: linux/amd64
- device: armnn
suffixes: '-armnn'
platforms: linux/arm64
- device: rknn
suffixes: '-rknn'
platforms: linux/arm64
- device: rocm
suffixes: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0
include: *ml-variants
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-machine-learning
image: ${{ matrix.image }}
context: machine-learning
dockerfile: machine-learning/Dockerfile
platforms: ${{ matrix.platforms }}
runner-mapping: ${{ matrix.runner-mapping }}
suffixes: ${{ matrix.suffixes }}
dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
tags: &tag-rules |
type=ref,event=branch
type=ref,event=pr
type=sha,format=long,prefix=commit-
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}},prefix=v
type=match,pattern=v(\d+),group=1,prefix=v,suffix=-rc,enable=${{ github.event.release.prerelease == true }}
type=raw,value=release,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
build-args: |
DEVICE=${{ matrix.device }}
@ -155,25 +165,72 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0
permissions:
contents: read
actions: read
packages: write
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
image: immich-server
context: .
dockerfile: server/Dockerfile
dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
tags: *tag-rules
build-args: |
DEVICE=cpu
mirror:
name: Mirror to Docker Hub
needs: [machine-learning, server]
if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }}
permissions:
packages: read
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Slightly hacky; we reuse the ml matrix but tack on immich-server too
image: [immich-server]
include: *ml-variants
env:
IMAGE: ${{ matrix.image }}
SUFFIX: ${{ matrix.suffixes }}
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
flavor: |
latest=false
images: ${{ env.GHCR_NAMESPACE }}/${{ env.IMAGE }}
tags: *tag-rules
- name: Mirror tags to Docker Hub
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
for tag in ${TAGS}; do
version="${tag##*:}"
docker buildx imagetools create \
-t "${DOCKERHUB_NAMESPACE}/${IMAGE}:${version}${SUFFIX}" \
"${GHCR_NAMESPACE}/${IMAGE}:${version}${SUFFIX}"
done
success-check-server:
name: Docker Build & Push Server Success
needs: [server, retag_server]
needs: [server, retag_server, mirror]
permissions: {}
runs-on: ubuntu-latest
if: always()
@ -184,7 +241,7 @@ jobs:
success-check-ml:
name: Docker Build & Push ML Success
needs: [machine-learning, retag_ml]
needs: [machine-learning, retag_ml, mirror]
permissions: {}
runs-on: ubuntu-latest
if: always()

View file

@ -21,14 +21,15 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@ -54,19 +55,20 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}

View file

@ -20,10 +20,12 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-actions: read
permission-pull-requests: read
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
run: echo 'The triggering workflow did not succeed' && exit 1
@ -126,19 +128,22 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-actions: read
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}

View file

@ -17,19 +17,21 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}

56
.github/workflows/fdroid.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Update F-Droid Repo
on:
release:
types: [published]
permissions: {}
jobs:
update-index:
runs-on: ubuntu-latest
if: ${{ !github.event.release.prerelease }}
permissions:
contents: read
steps:
- name: Checkout pubspec for versionCode
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false
sparse-checkout: mobile/pubspec.yaml
sparse-checkout-cone-mode: false
- name: Update F-Droid repo
env:
GITLAB_TOKEN: ${{ secrets.FDROID_REPO_TOKEN }}
RELEASE: ${{ toJSON(github.event.release) }}
TAG: ${{ github.event.release.tag_name }}
run: |
VERSION_CODE=$(yq '.version' mobile/pubspec.yaml | cut -d+ -f2)
export VERSION_CODE
NEW_ENTRY=$(yq -P '
[.assets[] | select(.name == "app-release.apk")] as $matches |
with(select($matches | length == 0); error("app-release.apk not found in release assets")) |
{
"url": $matches[0].browser_download_url,
"sha256sum": ($matches[0].digest | sub("^sha256:", "")),
"date": (.published_at | split("T") | .[0]),
"version-code": env(VERSION_CODE)
} | .date tag= "!!timestamp"
' <<< "$RELEASE")
export NEW_ENTRY
git clone --depth 1 "https://oauth2:${GITLAB_TOKEN}@gitlab.futo.org/fdroid/repo-v2.git" fdroid-repo
cd fdroid-repo
yq -i '
with(select([.apks[] | select(."version-code" == env(VERSION_CODE))] | length > 0); error("version-code already in index")) |
.apks = [env(NEW_ENTRY)] + .apks
' apps/Immich/index.yml
git config user.name "Immich Release Bot"
git config user.email "bot@immich.app"
git commit -am "Immich: add version-code ${VERSION_CODE} (${TAG})"
git push

View file

@ -15,20 +15,22 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: true
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}

View file

@ -35,6 +35,8 @@ jobs:
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Find translation PR
id: find_pr

View file

@ -14,13 +14,14 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5.6.0
with:
token: ${{ steps.token.outputs.token }}
mode: exactly

View file

@ -12,11 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
permission-pull-requests: write
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with:
repo-token: ${{ steps.token.outputs.token }}

View file

@ -3,8 +3,8 @@ name: Prepare new release
on:
workflow_dispatch:
inputs:
serverBump:
description: 'Bump server version'
releaseType:
description: 'Release type'
required: true
default: 'false'
type: choice
@ -17,10 +17,6 @@ on:
- prepatch
- prerelease
- release
mobileBump:
description: 'Bump mobile build number'
required: false
type: boolean
skipTranslations:
description: 'Skip translations'
required: false
@ -54,32 +50,32 @@ jobs:
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ steps.token.outputs.token }}
persist-credentials: true
ref: main
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
# TODO move to mise
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Bump version
env:
SERVER_BUMP: ${{ inputs.serverBump }}
MOBILE_BUMP: ${{ inputs.mobileBump }}
run: pnpm --silent release -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
RELEASE_TYPE: ${{ inputs.releaseType }}
run: mise //:release --type "${RELEASE_TYPE}"
- id: output
run: |
@ -106,6 +102,8 @@ jobs:
contents: read
pull-requests: write
secrets:
PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
KEY_JKS: ${{ secrets.KEY_JKS }}
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
@ -135,9 +133,11 @@ jobs:
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: write
permission-actions: read
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
@ -149,19 +149,23 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
draft: true
prerelease: ${{ needs.bump_version.outputs.rc }}
tag_name: ${{ needs.bump_version.outputs.version }}
token: ${{ steps.generate-token.outputs.token }}
generate_release_notes: true
body_path: misc/release/notes.tmpl
files: |
docker/docker-compose.yml
docker/docker-compose.rootless.yml
docker/example.env
docker/hwaccel.ml.yml
docker/hwaccel.transcoding.yml
docker/prometheus.yml
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
REPO: ${{ github.repository }}
TAG: ${{ needs.bump_version.outputs.version }}
PRERELEASE: ${{ needs.bump_version.outputs.rc }}
run: |
gh release create "${TAG}" \
--repo "${REPO}" \
--draft \
--verify-tag \
--prerelease="${PRERELEASE}" \
--notes-file misc/release/notes.tmpl \
--generate-notes \
docker/docker-compose.yml \
docker/docker-compose.rootless.yml \
docker/example.env \
docker/hwaccel.ml.yml \
docker/hwaccel.transcoding.yml \
docker/prometheus.yml \
*.apk

View file

@ -14,10 +14,11 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
with:
@ -32,10 +33,11 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: write
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:

View file

@ -16,19 +16,20 @@ jobs:
packages: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}

View file

@ -20,19 +20,21 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
mobile:
- 'mobile/**'
- 'i18n/en.json'
force-filters: |
- '.github/workflows/static_analysis.yml'
force-events: 'workflow_dispatch,release'
@ -49,58 +51,33 @@ jobs:
working-directory: ./mobile
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./mobile
- name: Install dependencies
run: flutter pub get
run: mise //mobile:install:ci
- name: Install dependencies for UI package
run: flutter pub get
run: flutter pub get --enforce-lockfile
working-directory: ./mobile/packages/ui
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Run Build Runner
run: mise //mobile:codegen:dart
- name: Generate platform API
run: mise //mobile:codegen:pigeon
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
mobile/**/*.g.dart
mobile/**/*.gr.dart
mobile/**/*.drift.dart
mobile/**/*.g.swift
mobile/**/*.g.kt
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
echo "ERROR: Generated files not up to date! Run 'mise //mobile:codegen:dart' and 'mise //mobile:codegen:pigeon'"
echo "Changed files: ${CHANGED_FILES}"
exit 1
- name: Perform codegen
run: mise //mobile:codegen
- name: Run analyze
run: mise //mobile:analyze

View file

@ -17,19 +17,20 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
root:
- 'misc/**'
scripts:
- 'packages/scripts/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
i18n:
@ -45,6 +46,8 @@ jobs:
- 'server/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
- 'packages/plugin-core/**'
- 'packages/plugin-sdk/**'
cli:
- 'packages/cli/**'
- 'packages/sdk/**'
@ -66,33 +69,34 @@ jobs:
- '.github/workflows/test.yml'
force-events: 'workflow_dispatch'
root-unit-tests:
name: Test the root workspace
script-unit-tests:
name: Scripts unit tests
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).root == true }}
if: ${{ fromJSON(needs.pre-job.outputs.should_run).scripts == true }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
- name: Run unit tests
run: pnpm test
run: pnpm --filter @immich/scripts test
server-unit-tests:
name: Test & Lint Server
@ -103,19 +107,20 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -134,19 +139,20 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -165,19 +171,20 @@ jobs:
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -209,19 +216,20 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -247,19 +255,20 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -275,19 +284,20 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -325,19 +335,20 @@ jobs:
working-directory: ./e2e
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -357,20 +368,21 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -393,23 +405,24 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
@ -428,6 +441,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Start Docker Compose
id: docker
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
@ -435,13 +449,13 @@ jobs:
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test
if: ${{ !cancelled() }}
if: ${{ !cancelled() && steps.docker.outcome == 'success' }}
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
if: ${{ !cancelled() && steps.docker.outcome == 'success' }}
- name: Capture Docker logs
if: always()
@ -470,23 +484,24 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
@ -505,6 +520,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Docker build
id: docker
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
@ -512,7 +528,7 @@ jobs:
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web
if: ${{ !cancelled() }}
if: ${{ !cancelled() && steps.docker.outcome == 'success' }}
- name: Archive e2e test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@ -525,7 +541,7 @@ jobs:
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui
if: ${{ !cancelled() }}
if: ${{ !cancelled() && steps.docker.outcome == 'success' }}
- name: Archive ui test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@ -538,7 +554,7 @@ jobs:
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance
if: ${{ !cancelled() }}
if: ${{ !cancelled() && steps.docker.outcome == 'success' }}
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@ -578,28 +594,28 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
working_directory: ./mobile
- name: Install dependencies
run: flutter pub get
working-directory: ./mobile
run: mise //mobile:install:ci
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Perform codegen
run: mise //mobile:codegen
- name: Run tests
run: mise //mobile:test
@ -616,18 +632,19 @@ jobs:
working-directory: ./machine-learning
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -646,19 +663,20 @@ jobs:
working-directory: ./.github
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -676,12 +694,13 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
@ -697,19 +716,20 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -724,7 +744,6 @@ jobs:
id: verify-changed-files
with:
files: |
mobile/openapi
packages/sdk
open-api/immich-openapi-specs.json
@ -755,19 +774,20 @@ jobs:
- 5432:5432
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@3bca63ca3c15020293b36b51737a3ee2c773340b # use-mise-action-v3.1.0
uses: immich-app/devtools/actions/use-mise@06a9ef925332c91be647d6256642b86b398592c8 # use-mise-action-v3.2.1
with:
github_token: ${{ steps.token.outputs.token }}
@ -900,6 +920,6 @@ jobs:
# emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: true
# script: |
# flutter pub get
# flutter pub get --enforce-lockfile
# flutter test integration_test
# attempt_limit: 3

View file

@ -24,14 +24,15 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-contents: read
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@ -47,10 +48,11 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
uses: immich-app/devtools/actions/create-workflow-token@1af396ae134e4bc3b63d947e672bc68bf4ff9dc5 # create-workflow-token-action-v3.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
permission-pull-requests: read
- name: Bot review status
env:

5
.gitignore vendored
View file

@ -11,13 +11,10 @@ docker/library
uploads
coverage
mobile/generated
mobile/gradle.properties
mobile/openapi/pubspec.lock
mobile/*.jks
mobile/libisar.dylib
mobile/openapi/test
mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
mobile/ios/build
packages/**/build

View file

@ -12,6 +12,8 @@
"editorconfig.editorconfig",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"bluebrown.yamlfmt"
"bluebrown.yamlfmt",
// TODO TS 7 while it's still in preview
"typescriptteam.native-preview"
]
}

40
.vscode/settings.json vendored
View file

@ -29,9 +29,6 @@
"editor.formatOnSave": true,
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
},
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
@ -43,37 +40,42 @@
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript", "typescript", "svelte"],
"eslint.workingDirectories": [
{ "directory": "cli", "changeProcessCWD": true },
{ "directory": "e2e", "changeProcessCWD": true },
{ "directory": "server", "changeProcessCWD": true },
{ "directory": "web", "changeProcessCWD": true }
{ "changeProcessCWD": true, "directory": "cli" },
{ "changeProcessCWD": true, "directory": "e2e" },
{ "changeProcessCWD": true, "directory": "server" },
{ "changeProcessCWD": true, "directory": "web" }
],
"files.watcherExclude": {
"**/.jj/**": true,
"**/.git/**": true,
"**/node_modules/**": true,
"**/build/**": true,
"**/dist/**": true,
"**/.svelte-kit/**": true
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"*.js": "${capture}.spec.js,${capture}.mock.js",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
},
"files.watcherExclude": {
"**/.git/**": true,
"**/.jj/**": true,
"**/.svelte-kit/**": true,
"**/build/**": true,
"**/dist/**": true,
"**/node_modules/**": true
},
// TODO remove once the ts7 extension is stable
"js/ts.experimental.useTsgo": true,
"js/ts.preferences.importModuleSpecifier": "non-relative",
"search.exclude": {
"**/node_modules": true,
"**/.svelte-kit": true,
"**/build": true,
"**/dist": true,
"**/.svelte-kit": true,
"**/node_modules": true,
"**/open-api/typescript-sdk/src": true
},
"svelte.enable-ts-plugin": true,
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"tailwindCSS.experimental.configFile": {
"web/src/app.css": "web/src/**"
},
"js/ts.preferences.importModuleSpecifier": "non-relative",
"vitest.maximumConfigs": 10
}

View file

@ -4,4 +4,4 @@
/web/ @danieldietzler
/machine-learning/ @mertalev
/e2e/ @danieldietzler
/mobile/ @shenlong-tanwen @santoshakil
/mobile/ @shenlong-tanwen @santoshakil @agg23

View file

@ -19,7 +19,13 @@ We usually do not assign issues to new contributors, since it happens often that
## Use of generative AI
We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request.
We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. Even a one line change can have significant impact. We cannot have any confidence in an LLM, so if it's non-trivial for us to verify it works and you don't show that you fully understand all implications of the change, reviewing the PR is not worth our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request.
If you use an LLM to translate a PR description or title, that is fine. Please make sure however that you stick to the PR template and that the text is written concisely.
LLMs must not be used to fix GitHub issues labelled good first issue. These issues are generally not urgent, and are intended to be learning opportunities for new contributors to get familiar with the codebase. Whether you are a newcomer or not, fully automating the process of fixing this issue squanders the learning opportunity and doesnt add much value to the project. Using LLMs to fix issues labelled as “good first issue” is forbidden, and those PRs will be closed automatically.
Misrepresenting LLM use, contribution farming (automated low-effort PRs), or repeatedly hitting auto-close rules may be grounds for a block at maintainer discretion.
## Feature freezes
@ -38,7 +44,7 @@ All our translations are done through [Weblate](https://hosted.weblate.org/proje
### Datasets
Help us improve our [Immich Datasets](https://datasets.immich.app) by submitting photos and videos taken from a variety of devices, including smartphones, DSLRs, and action cameras, as well as photos with unique features, such as panoramas, burst photos, and photo spheres. These datasets will be publically available for anyone to use, do not submit private/sensitive photos.
Help us improve our [Immich Datasets](https://datasets.immich.app) by submitting photos and videos taken from a variety of devices, including smartphones, DSLRs, and action cameras, as well as photos with unique features, such as panoramas, burst photos, and photo spheres. These datasets will be publicly available for anyone to use, do not submit private/sensitive photos.
### Community support

View file

@ -1,58 +0,0 @@
dev:
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
dev-down:
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
dev-update:
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
dev-scale:
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
dev-docs:
npm --prefix docs run start
.PHONY: e2e
e2e:
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
e2e-dev:
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
e2e-update:
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
e2e-down:
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
prod:
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
prod-down:
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
prod-scale:
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
.PHONY: open-api
open-api:
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
sql:
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
renovate:
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
# Include .env file if it exists
-include docker/.env
MODULES = e2e server web cli sdk docs .github
test-e2e:
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
clean:
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1

View file

@ -32,11 +32,13 @@
<a href="readme_i18n/README_zh_TW.md">正體中文</a>
<a href="readme_i18n/README_uk_UA.md">Українська</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_bg_BG.md">Български</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
<a href="readme_i18n/README_ml_IN.md">മലയാളം</a>
</p>

View file

@ -1,36 +1,43 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.opentofu]]
version = "1.11.6"
version = "1.12.5"
backend = "aqua:opentofu/opentofu"
[tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545"
[tools.opentofu."platforms.linux-x64"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624"
[tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544"
[tools.opentofu."platforms.macos-x64"]
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621"
[tools.opentofu."platforms.windows-x64"]
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547"
[[tools.terragrunt]]
version = "1.0.3"
@ -39,27 +46,34 @@ backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412"

View file

@ -1,6 +1,6 @@
[tools]
terragrunt = "1.0.3"
opentofu = "1.11.6"
terragrunt = "1.1.1"
opentofu = "1.12.5"
[tasks."tg:fmt"]
run = "terragrunt hclfmt"

View file

@ -2,37 +2,35 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.7"
constraints = "4.52.7"
version = "4.52.8"
constraints = "4.52.8"
hashes = [
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
]
}

View file

@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.7"
version = "4.52.8"
}
}
}

View file

@ -2,37 +2,35 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.7"
constraints = "4.52.7"
version = "4.52.8"
constraints = "4.52.8"
hashes = [
"h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
"h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
"h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
"h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
"h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
"h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
"h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
"h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
"h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
"h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
"h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
"h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
"h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
"h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
"zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
"zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
"zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
"zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
"zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"h1:2uxVaMaGPDJljXqYzuD6xtyfOKdNudtGIgawRTkFJIs=",
"h1:34HL1GDpqYdn2iEC+85pCi+NddZf30qH3fJ8y3P6CAM=",
"h1:4Z+uD+YY/7HrqdypM7DyUK+RtWepoLBc9fqtRQYZx4I=",
"h1:8Ss6wlJL2BHvkIZU1iaHvyUntqNhPFZ0fl6mjsxhctc=",
"h1:AYeWWMTWaNg01N3hivxr/6YlG41j1RietMnft9vLjUI=",
"h1:BGRNOzo8NUgbx5RwpWkWmr38f/s3txb7mzQhqfM5blI=",
"h1:EEXRMdC8bPlpLuOU8u+dE8mFJe3BpquFdH6vXcKseY4=",
"h1:ENp29e6O6OnaHJlbtmRlyjlAmm3Wt4mGctWUpBY3d0g=",
"h1:IAtBAbGVe1npj/n1NvCIZkSYGl7qaPYT+FnVdZ2CZ00=",
"h1:QRGGKngrxLORr4CImKXLDDJL5s67zUNBDAgk6FIIFNk=",
"h1:bNql6cC+MfDbaJK3q7a8C1m3A21o7e5mrjBIGtk0RdI=",
"h1:gmNv+PEOkYDdu+c15bfpcoAE3EI7pgmov9RMf3GZLsI=",
"h1:yieZ7NWRYQcLYHUevzDDrrmDkoQICM+3ZeX4i/mw9ME=",
"zh:08b305329a680a9213b2d8e642fbce7e4d97a524b1d2cef59e190ba9d678c477",
"zh:47975bd711ee18a46e589822171fa87474a552b332bfc8dea8fd1a64504eed8d",
"zh:5640d0d226bbafff3395542456c29feb942ca9c55ac01b4245a34c0590a33363",
"zh:5b0ad839fafba938c60a95d6b4a865843643591e97573ed2b9c4af3754064f74",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
"zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
"zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
"zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
"zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
"zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
"zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
"zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
"zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
"zh:93a9bc1139f5c02a44fdbf51fb2ce0891e2a42033b66febb928ccf72e870408f",
"zh:977f75cdf365686aa16ae02dcc0fc1769bae6f86be6e165393756c940bfb4af0",
"zh:9afcda2660b3dc6ee6329acea532a719225bd1f6cf46f695feb2d77160847c1b",
"zh:9e3da67b1b05b03d1f0c18b8677edf3797815b5dd49629f10eb2f457dccbed30",
"zh:b8d7da230f5266367c1b6b1cf31aa39087e231a87d55b71bb9d5c854ca1fcfe6",
"zh:cc96e7cd7350456b7a11a53d1c76c73a542de8359f9750f637ff865e8e77be91",
"zh:da1f58d067def243047bb7178cb197e2b9c3a791a9eb380ad92b73290615ae29",
"zh:ed5f3e1f59a338bcdce0a2537a8a2f299cfe18c168626799b2a9c97af3d8d3cb",
"zh:f16cc31f73a58a26ffe2d93223eddb7c340623a4e2ef38c6091766fd611e3e11",
]
}

View file

@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.7"
version = "4.52.8"
}
}
}

View file

@ -154,7 +154,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:3acc0687f2a2e1091fae6450d7842dd658c941338cf0a873ddd9e14b9e4ea4dd
healthcheck:
test: redis-cli ping || exit 1

View file

@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:3acc0687f2a2e1091fae6450d7842dd658c941338cf0a873ddd9e14b9e4ea4dd
healthcheck:
test: redis-cli ping || exit 1
restart: always
@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
image: prom/prometheus@sha256:508729e0e2d18e11fd742a5a5ca70e557b940a93948c3c95fd0123a6fd538b69
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@ -97,7 +97,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:12.4.4-ubuntu@sha256:df2e7ef5f32f771794cf76bad5f2bceac227036460a2cc269a9045e5662abc58
image: grafana/grafana:12.4.7-ubuntu@sha256:d327f509400334fbea74a282d287f20c3187222741ec99e76f253cac16c90129
volumes:
- grafana-data:/var/lib/grafana

View file

@ -51,8 +51,6 @@ services:
- NET_RAW
volumes:
- ./ml-model-cache:/cache
- ./ml-dotcache:/.cache
- ./ml-config:/.config
env_file:
- .env
restart: always
@ -61,7 +59,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:3acc0687f2a2e1091fae6450d7842dd658c941338cf0a873ddd9e14b9e4ea4dd
user: '1000:1000'
security_opt:
- no-new-privileges:true

View file

@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:3acc0687f2a2e1091fae6450d7842dd658c941338cf0a873ddd9e14b9e4ea4dd
healthcheck:
test: redis-cli ping || exit 1
restart: always

View file

@ -10,7 +10,7 @@ DB_DATA_LOCATION=./postgres
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v2.1.0"
IMMICH_VERSION=v2
IMMICH_VERSION=v3
# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces

View file

@ -409,12 +409,10 @@ To decrease Redis logs, you can add the following line to the `redis:` section o
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
[Example docker-compose.yml file](https://github.com/immich-app/immich/blob/main/docker/docker-compose.rootless.yml)
[Example docker-compose.rootless.yml file](https://github.com/immich-app/immich/blob/main/docker/docker-compose.rootless.yml)
You may need to add mount points or docker volumes for the following internal container paths:
- `immich-machine-learning:/.config`
- `immich-machine-learning:/.cache`
- `redis:/data`
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.

View file

@ -31,7 +31,7 @@ You can trigger a database backup manually:
1. Go to **Administration > Job Queues**
2. Click **Create job** in the top right
3. Select **Create Database Backup** and click **Confirm**
3. Select **Create Database Dump** and click **Confirm**
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.

View file

@ -14,6 +14,8 @@ Under Email, enter the required details to connect with an SMTP server.
You can use [this guide](/guides/smtp-gmail) to use Gmail's SMTP server.
You can use [this guide](/guides/smtp-microsoft365) to use Microsoft's SMTP server.
## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -280,7 +280,7 @@ Configuration of OAuth in Immich System Settings
| ---------------------------- | ----------------------------------------------------- |
| Issuer URL | `https://<KEYCLOAK_DOMAIN>/realms/<YOUR_REALM>` |
| Client ID | immich |
| Client Secret | can be optained from Clients -> immich -> Credentials |
| Client Secret | can be obtained from Clients -> immich -> Credentials |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |

View file

@ -35,6 +35,7 @@ Found Admin:
- Email=admin@example.com
- Name=Immich Admin
? Please choose a new password (optional) immich-is-cool
? Invalidate existing sessions? Yes
The admin password has been updated.
```

View file

@ -1,5 +1,30 @@
# System Integrity
## Integrity report
At a [customizable interval](https://my.immich.app/admin/system-settings?isOpen=integrity-checks), Immich runs integrity checks to ensure that your library is still intact and there are no corrupt files.
There are three kind of issues Immich checks for:
- Untracked files: the path was found in Immich's directories but it is not referenced in Immich's database
- Missing files: the path is found in the Immich internal database, but does not actually exist on disk
- Checksum mismatches: the checksum of the file stored in Immich's database does not match the actual file's checksum anymore
All three run nightly at 3am by default. For the "Checksum files" check, there are additional time and progress limits, as those are the most taxing checks. With these additional limits, Immich can slowly check checksums of your files over the course of a couple of days.
You can see the results of these checks on the [maintenance](https://my.immich.app/admin/maintenance) page. Here, you can also trigger a full scan (a _check_) for specific jobs, or all of them. In addition, you can also _refresh_ checks. This will only look at items that have currently been reported on, and check if those have been fixed.
### Common causes
Most common are untracked files. In many cases those are corrupted thumbnails or encoded videos that have been partially generated at some point and never got cleaned up properly. These are usually fine to delete, as both can always be regenerated at a later point. Other files will need to be investigated on a case-by-case basis by checking they already exist in Immich and thinking about how they might have gotten untracked.
:::info
You might want to run the _missing_ jobs for thumbnails and encoded videos (https://my.immich.app/admin/queues) to make sure all your assets have proper thumbnails and encoded videos. Watch the server logs while running the jobs, in case there are any issues with some assets.
:::
Missing files are files where Immich references them internally, but they don't actually exist on disk in that location. It could be that you deleted a file on disk from the internal library (don't do that, Immich doesn't support it). It could also be that there are issues with your file storage. Please carefully investigate missing files, and never hesitate to reach out on [our Discord](https://discord.immich.app) if you have any questions!
Checksum mismatches are often indicative of file system corruption. It could also be that you previously edited a file from the internal library on the disk, which is also not supported and will cause a checksum mismatch. Again, the recommended action is to look at any reported item individually, check it out, try to remember if you changed it or some metadata on it at some point. If you edited the file, the supported resolution is to delete the mismatched asset from Immich and reupload it as a new asset.
## Folder checks
:::info
@ -40,7 +65,7 @@ The above error messages show that the server has previously (successfully) writ
### Ignoring the checks
:::warning
:::danger
The checks are designed to catch common problems that we have seen users have in the past, and often indicate there's something wrong that you should solve. If you know what you're doing and you want to disable them you can set the following environment variable:
:::

View file

@ -10,4 +10,4 @@ OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generato
mise open-api
```
You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/generated/openapi` for Dart SDK.

View file

@ -12,11 +12,11 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
| `.vscode/` | VSCode debug launch profiles |
| `packages/cli` | Source code for the CLI |
| `packages/sdk` | Source code for the generated OpenAPI SDK |
| `packages/scripts` | Scripts for version bumps and draft releases |
| `docker/` | Docker compose resources for dev, test, production |
| `design/` | Screenshots and logos for the README |
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
| `machine-learning/` | Source code for the `immich-machine-learning` docker image |
| `misc/release/` | Scripts for version bumps and draft releases |
| `mobile/` | Source code for the mobile app, both Android and iOS |
| `server/` | Source code for the `immich-server` docker image |
| `web/` | Source code for the `web` |

View file

@ -30,7 +30,7 @@ This environment includes the services below. Additional details are available i
- Redis
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
All the services are packaged to run as with single Docker Compose command.
All the services are packaged to run with a single Docker Compose command.
:::tip mise
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
@ -41,7 +41,7 @@ All the services are packaged to run as with single Docker Compose command.
1. Clone the project repo.
2. Run `cp docker/example.env docker/.env`.
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
4. Install dependencies - `pnpm i`
4. Install dependencies - `mise x -- pnpm i`
5. From the root directory, run:
```bash title="Start development server"
@ -52,7 +52,7 @@ mise dev
All the services will be started with hot-reloading enabled for a quick feedback loop.
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000/api`
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000`
**Notes:**
@ -99,6 +99,21 @@ To see local changes to `@immich/ui` in Immich, do the following:
2. Run `mise //mobile:translation` to generate the translation file.
3. Change to the `mobile/` directory and run `flutter run` to start the app.
##### iOS Code Signing
The Immich Apple Team ID and bundle IDs are specified in `mobile/ios/Signing.xcconfig`. For local development, we provide an override mechanism.
Create `mobile/ios/Signing.local.xcconfig` and populate it with the necessary values needed to build and sign Immich yourself. This local override file is gitignored.
```
IMMICH_TEAM_ID = ABCDE12345
IMMICH_BUNDLE_ID_PROD = com.customuniqueid.immich
IMMICH_BUNDLE_ID_DEV = com.customuniqueid.immichdev
IMMICH_GROUP_ID = group.com.customuniqueid.immich
```
The environment values are used across Immich's targets and schemes to prevent redundant edits by contributors.
#### Translation
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then run:

View file

@ -99,6 +99,7 @@ Options:
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
--visibility <visibility> Set the visibility of uploaded assets (choices: "archive", "timeline", "hidden", "locked", env: IMMICH_VISIBILITY)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
@ -176,6 +177,12 @@ By default, hidden files are skipped. If you want to include hidden files, use t
immich upload --include-hidden --recursive directory/
```
You can set the visibility of uploaded assets to `archive`, `timeline`, `hidden`, or `locked` with the `--visibility` option:
```bash
immich upload --visibility archive --recursive directory/
```
You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
output you will need to strip the first three lines of output to get the json.

View file

@ -15,14 +15,14 @@ When using "Deduplicate All" or viewing suggestions, Immich automatically presel
### Synchronizing metadata
When resolving duplicates, metadata from trashed assets is automatically synchronized to the kept assets. The following metadata is synchronized:
When resolving duplicates, metadata from trashed assets is automatically synchronized to the kept asset. This synchronization only happens when **exactly one** asset is kept and at least one asset is trashed. When more than one asset is kept, metadata is not merged — the assets keep their own metadata and are simply removed from the duplicate group. The following metadata is synchronized:
| Name | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Album | The kept assets will be added to _every_ album that the other assets in the group belong to. |
| Favorite | If any of the assets in the group have been added to favorites, every kept asset will also be added to favorites. |
| Rating | If one or more assets in the duplicate group have a rating, the highest rating is selected and synchronized to the kept assets. |
| Description | Descriptions from each asset are combined together and synchronized to all the kept assets. |
| Visibility | The most restrictive visibility is applied to the kept assets. |
| Location | Latitude and longitude are copied if all assets with geolocation data in the group share the same coordinates. |
| Tag | Tags from all assets in the group are merged and applied to every kept asset. |
| Name | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Album | The kept asset will be added to _every_ album that the other assets in the group belong to. |
| Favorite | If any of the assets in the group have been added to favorites, the kept asset will also be added to favorites. |
| Rating | If one or more assets in the duplicate group have a rating, the highest rating is selected and synchronized to the kept asset. |
| Description | Descriptions from each asset are combined together and synchronized to the kept asset. |
| Visibility | The most restrictive visibility is applied to the kept asset. |
| Location | Latitude and longitude are copied if all assets with geolocation data in the group share the same coordinates. |
| Tag | Tags from all assets in the group are merged and applied to the kept asset. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View file

@ -0,0 +1,19 @@
# SMTP settings using Microsoft 365
This guide walks you through how to get the information you need to set up your Immich instance to send emails using Microsoft's SMTP server.
## Create an app password
You will need to generate an app password to use your Microsoft email in Immich. Depending on if you have a personal or business account, you can use https://go.microsoft.com/fwlink/?linkid=2274139 or https://myaccount.microsoft.com/securtiy-info respectively.
## Entering the SMTP credential in Immich
Entering your credential in Immich's email notification settings at `Administration -> Settings -> Notification Settings`
Host: smtp-mail.outlook.com
Port: 587
username: your mail address
Password: app password you created earlier
SMTPS: set it to disabled
<img src={require('./img/email-ms-settings.webp').default} width="80%" title="SMTP settings" />

View file

@ -6,14 +6,18 @@ sidebar_position: 100
A config file can be provided as an alternative to the UI configuration.
:::note Interaction with the web UI
While the config file does not need to include all keys from the below example, specifying `IMMICH_CONFIG_FILE` will disable the ability to edit other properties from the Immich web UI.
:::
### Step 1 - Create a new config file
In JSON format, create a new config file (e.g. `immich.json`) and put it in a location mounted in the container that can be accessed by Immich.
In JSON format, create a new config file (e.g. `immich-config.json`) and put it in a location mounted in the container that can be accessed by Immich.
YAML-formatted config files are also supported.
The default configuration looks like this:
<details>
<summary>immich.json</summary>
<summary>immich-config.json</summary>
```json
{
@ -250,6 +254,10 @@ So you can just grab it from there, paste it into a file and you're pretty much
### Step 2 - Specify the file location
:::note
If you have any `microservices` workers, they will also need to have the config file mounted to their container.
:::
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
For more information, refer to the [Environment Variables](/install/environment-variables.md) section.
@ -261,7 +269,7 @@ It is recommended to reuse this variable in your `docker-compose.yml`:
```yaml
volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
- ./immich-config.json:${IMMICH_CONFIG_FILE}
```
:::

View file

@ -19,7 +19,7 @@ If this does not work, try running `docker compose up -d --force-recreate`.
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-----: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
| `IMMICH_VERSION` | Image tags | `v3` | server, machine learning |
| `UPLOAD_LOCATION` | Host path for uploads | | server |
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
@ -45,7 +45,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` and `/admin/database-backups/start-restore` endpoints | `true` | server | api |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
@ -89,7 +89,7 @@ Information on the current workers can be found [here](/administration/jobs-work
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
\*2: If not provided, the appropriate extension to use is auto-detected at startup by inspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvector.
\*3: Uses either [`postgresql.ssd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.ssd.conf) or [`postgresql.hdd.conf`](https://github.com/immich-app/base-images/blob/main/postgres/postgresql.hdd.conf) which mainly controls the Postgres `effective_io_concurrency` setting to allow for concurrenct IO on SSDs and sequential IO on HDDs.

View file

@ -7,7 +7,7 @@ sidebar_position: 85
:::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
Community support should be directed to Synology-specific support platforms.
:::
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.

View file

@ -1,13 +1,19 @@
---
sidebar_position: 60
sidebar_position: 70
---
# Unraid
# Unraid [ Community ]
:::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
:::
Immich can easily be installed and updated on Unraid via:
1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
2. Community made template on the Unraid Community Apps
1. Community made template on the Unraid Community Apps
2. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps
## Community Applications Template
@ -23,7 +29,7 @@ Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA,
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
## Docker-Compose Method (Official)
## Docker-Compose Method
:::info
@ -121,7 +127,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
width="90%"
alt="Go to Docker Tab and visit the address listed next to immich-web"
/>
</details>
:::tip

View file

@ -28,11 +28,15 @@ docker image prune
## Versioning Policy
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`.
We intend for breaking changes, including those to the API or deployment, to be limited to major version releases.
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`. These metatags do not follow release candidates.
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
Switching back to an earlier version, even within the same minor release tag, is not supported.
The mobile app is typically compatible with the current and prior major version. However, the server is only compatible with the matching major version.
Thus, we recommend upgrading all mobile clients before upgrading the server to ensure compatibility.
We do not backport patches to earlier versions. We encourage all users to run the most recent stable release of Immich.
Downgrading to an earlier version, even within the same minor version, is not supported.
[semver]: https://semver.org/

View file

@ -1,7 +1,7 @@
The mobile app can be downloaded from the following places:
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
- [Apple App Store](https://apps.apple.com/us/app/immich/id1613945652)
- [F-Droid](https://f-droid.org/packages/app.alextran.immich)
- [GitHub Releases (apk)](https://github.com/immich-app/immich/releases)
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
- [GitHub Releases (APK)](https://github.com/immich-app/immich/releases)
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
- [F-Droid](https://app.futo.org/fdroid/repo/)

View file

@ -191,7 +191,7 @@ const config = {
},
{
label: 'Privacy Policy',
to: '/privacy-policy',
href: 'https://immich.app/privacy-policy',
},
],
},

View file

@ -1,7 +1,7 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.wrangler]]
version = "4.100.0"
version = "4.110.0"
backend = "npm:wrangler"
[tools.wrangler.options]

View file

@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.100.0"
wrangler = "4.119.0"

View file

@ -40,7 +40,8 @@
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.10.0",
"prettier": "^3.7.4",
"typescript": "^6.0.0"
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2"
},
"browserslist": {
"production": [

View file

@ -1,78 +0,0 @@
import useIsBrowser from '@docusaurus/useIsBrowser';
import { mdiCheckboxBlankCircle, mdiCheckboxMarkedCircle } from '@mdi/js';
import Icon from '@mdi/react';
import React from 'react';
export type Item = {
icon: string;
iconColor: string;
title: string;
description?: string;
link?: { url: string; text: string };
done?: false;
getDateLabel: (language: string) => string;
};
interface Props {
items: Item[];
}
export function Timeline({ items }: Props): JSX.Element {
const isBrowser = useIsBrowser();
return (
<ul className="flex flex-col pl-4">
{items.map((item, index) => {
const isFirst = index === 0;
const isLast = index === items.length - 1;
const done = item.done ?? true;
const dateLabel = item.getDateLabel(isBrowser ? navigator.language : 'en-US');
const timelineIcon = done ? mdiCheckboxMarkedCircle : mdiCheckboxBlankCircle;
const cardIcon = item.icon;
return (
<li key={index} className={`flex min-h-24 w-[700px] max-w-[90vw] ${done ? '' : 'italic'}`}>
<div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
{dateLabel}
</div>
<div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
<div
className={`h-full border-solid border-4 border-immich-primary dark:border-immich-dark-primary ${
isFirst && 'rounded rounded-t-full'
} ${isLast && 'rounded rounded-b-full'}`}
></div>
</div>
<div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
{<Icon path={timelineIcon} size={1.25} />}
</div>
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-row w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="flex flex-col flex-grow justify-between gap-2">
<div className="flex gap-2 items-center">
{cardIcon === 'immich' ? (
<img src="/img/immich-logo.svg" height="30" className="rounded-none" />
) : (
<Icon path={cardIcon} size={1} color={item.iconColor} />
)}
<p className="m-0 mt-1 text-lg items-start flex gap-2 place-items-center content-center">
<span>{item.title}</span>
</p>
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</div>
<div className="flex flex-col justify-between place-items-end">
<span className="dark:text-immich-dark-primary text-immich-primary">
{item.link && (
<a href={item.link.url} target="_blank" rel="noopener">
[{item.link.text}]
</a>
)}
</span>
<div className="md:hidden text-sm text-right">{dateLabel}</div>
</div>
</section>
</li>
);
})}
</ul>
);
}

View file

@ -2,6 +2,8 @@ import { useWindowSize } from '@docusaurus/theme-common';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import React, { useEffect, useState } from 'react';
const ARCHIVED_VERSIONS_URL = 'https://version.immich.cloud/v1/docs/versions';
export default function VersionSwitcher(): JSX.Element {
const [versions, setVersions] = useState([]);
const [activeLabel, setLabel] = useState('Versions');
@ -11,12 +13,7 @@ export default function VersionSwitcher(): JSX.Element {
useEffect(() => {
async function getVersions() {
try {
let baseUrl = 'https://docs.immich.app';
if (window.location.origin === 'http://localhost:3005') {
baseUrl = window.location.origin;
}
const response = await fetch(`${baseUrl}/archived-versions.json`);
const response = await fetch(ARCHIVED_VERSIONS_URL);
const archiveVersions = await response.json();

View file

@ -1,110 +0,0 @@
import React from 'react';
import Layout from '@theme/Layout';
function HomepageHeader() {
return (
<header>
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">
<section>
<h1>Privacy Policy</h1>
<p>Last updated: July 31st 2024</p>
<p>
Welcome to Immich. We are committed to respecting your privacy. This Privacy Policy sets out how we collect,
use, and share information when you use our Immich app.
</p>
</section>
{/* 1. Scope of This Policy */}
<section>
<h2>1. Scope of This Policy</h2>
<p>
This Privacy Policy applies to the Immich app ("we", "our", or "us") and covers our collection, use, and
disclosure of your information. This Policy does not cover any third-party websites, services, or
applications that can be accessed through our app, or third-party services you may access through Immich.
</p>
</section>
{/* 2. Information We Collect */}
<section>
<h2>2. Information We Collect</h2>
<div>
<p>
<strong>Locally Stored Data</strong>: Immich stores all your photos, albums, settings, and locally on your
device. We do not have access to this data, nor do we transmit or store it on any of our servers.
</p>
</div>
<div>
<p>
<strong>Purchase Information:</strong> When you make a purchase within the{' '}
<a href="https://buy.immich.app">https://buy.immich.app</a>, we collect the following information for tax
calculation purposes:
</p>
<ul>
<li>Country of origin</li>
<li>Postal code (if the user is from Canada or the United States)</li>
</ul>
</div>
</section>
{/* 3. Use of Your Information */}
<section>
<h2>3. Use of Your Information</h2>
<p>
<strong>Tax Calculation:</strong> The country of origin and postal code (for users from Canada or the United
States) are collected solely for determining the applicable tax rates on your purchase.
</p>
</section>
{/* 4. Sharing of Your Information */}
<section>
<h2>4. Sharing of Your Information</h2>
<ul>
<li>
<strong>Tax Authorities:</strong> The purchase information may be shared with tax authorities as required
by law.
</li>
<li>
<strong>Payment Providers:</strong> The purchase information may be shared with payment providers where
required.
</li>
</ul>
</section>
{/* 5. Changes to This Policy */}
<section>
<h2>5. Changes to This Policy</h2>
<p>
We may update our Privacy Policy from time to time. If we make any changes, we will notify you by revising
the "Last updated" date at the top of this policy. It's encouraged that users frequently check this page for
any changes to stay informed about how we are helping to protect the personal information we collect.
</p>
</section>
{/* 6. Contact Us */}
<section>
<h2>6. Contact Us</h2>
<p>
If you have any questions about this Privacy Policy, please contact us at{' '}
<a href="mailto:immich@futo.org">immich@futo.org</a>
</p>
</section>
</section>
</header>
);
}
export default function Home(): JSX.Element {
return (
<Layout
title="Home"
description="immich Self-hosted photo and video backup solution directly from your mobile phone "
noFooter={true}
>
<HomepageHeader />
<div className="flex flex-col place-items-center place-content-center">
<p>This project is available under GNU AGPL v3 license.</p>
<p className="text-xs">Privacy should not be a luxury</p>
</div>
</Layout>
);
}

View file

@ -1,3 +1,4 @@
/archived-versions.json https://version.immich.cloud/v1/docs/versions 307
/ /overview/quick-start 307
/mobile-app-beta-program /features/mobile-app 307
/contribution-guidelines /overview/support-the-project#contributing 307
@ -36,3 +37,4 @@
/overview/welcome /overview/quick-start 307
/docs/* /:splat 307
/features/automatic-backup /features/mobile-backup 307
/privacy-policy https://immich.app/privacy-policy 307

View file

@ -1,261 +0,0 @@
[
{
"label": "v3.0.0-rc.2",
"url": "https://docs.v3.0.0-rc.2.archive.immich.app"
},
{
"label": "v2.7.5",
"url": "https://docs.v2.7.5.archive.immich.app"
},
{
"label": "v2.6.3",
"url": "https://docs.v2.6.3.archive.immich.app"
},
{
"label": "v2.5.6",
"url": "https://docs.v2.5.6.archive.immich.app"
},
{
"label": "v2.4.1",
"url": "https://docs.v2.4.1.archive.immich.app"
},
{
"label": "v2.3.1",
"url": "https://docs.v2.3.1.archive.immich.app"
},
{
"label": "v2.2.3",
"url": "https://docs.v2.2.3.archive.immich.app"
},
{
"label": "v2.1.0",
"url": "https://docs.v2.1.0.archive.immich.app"
},
{
"label": "v2.0.1",
"url": "https://docs.v2.0.1.archive.immich.app"
},
{
"label": "v1.144.1",
"url": "https://docs.v1.144.1.archive.immich.app"
},
{
"label": "v1.143.1",
"url": "https://docs.v1.143.1.archive.immich.app"
},
{
"label": "v1.142.1",
"url": "https://v1.142.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.141.1",
"url": "https://v1.141.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.140.1",
"url": "https://v1.140.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.139.4",
"url": "https://v1.139.4.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.138.1",
"url": "https://v1.138.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.137.3",
"url": "https://v1.137.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.136.0",
"url": "https://v1.136.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.135.3",
"url": "https://v1.135.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.134.0",
"url": "https://v1.134.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.133.1",
"url": "https://v1.133.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.131.3",
"url": "https://v1.131.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.130.3",
"url": "https://v1.130.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.129.0",
"url": "https://v1.129.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.128.0",
"url": "https://v1.128.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.127.0",
"url": "https://v1.127.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.123.0",
"url": "https://v1.123.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.122.3",
"url": "https://v1.122.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.121.0",
"url": "https://v1.121.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.120.2",
"url": "https://v1.120.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.119.1",
"url": "https://v1.119.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.118.2",
"url": "https://v1.118.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.117.0",
"url": "https://v1.117.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.114.0",
"url": "https://v1.114.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.113.1",
"url": "https://v1.113.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.111.0",
"url": "https://v1.111.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.110.0",
"url": "https://v1.110.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app",
"rootPath": "/docs"
},
{
"label": "v1.100.0",
"url": "https://v1.100.0.archive.immich.app",
"rootPath": "/docs"
}
]

View file

@ -11,15 +11,9 @@ module.exports = {
colors: {
// Light Theme
'immich-primary': '#4250af',
'immich-bg': '#f9f8fb',
'immich-fg': 'black',
'immich-gray': '#F6F6F4',
// Dark Theme
'immich-dark-primary': '#adcbfa',
'immich-dark-bg': '#000000',
'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#111111',
},
},
},

View file

@ -45,7 +45,7 @@ services:
redis:
container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:3acc0687f2a2e1091fae6450d7842dd658c941338cf0a873ddd9e14b9e4ea4dd
healthcheck:
test: redis-cli ping || exit 1

View file

@ -2,13 +2,8 @@ import js from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import typescriptEslint from 'typescript-eslint';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default typescriptEslint.config([
eslintPluginUnicorn.configs.recommended,
eslintPluginPrettierRecommended,
@ -29,7 +24,7 @@ export default typescriptEslint.config([
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
tsconfigRootDir: import.meta.dirname,
},
},
@ -41,9 +36,18 @@ export default typescriptEslint.config([
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
'unicorn/consistent-boolean-name': 'off',
'unicorn/no-non-function-verb-prefix': 'off',
'unicorn/no-unreadable-for-of-expression': 'off',
'unicorn/max-nested-calls': 'off',
'unicorn/prefer-uint8array-base64': 'off',
'unicorn/isolated-functions': 'off',
'unicorn/prefer-promise-with-resolvers': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
'unicorn/prefer-simple-condition-first': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/prevent-abbreviations': 'off',
'unicorn/name-replacements': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',

View file

@ -1,5 +1,5 @@
[tasks.install]
run = "pnpm install --filter immich-e2e --frozen-lockfile"
run = "pnpm install --filter immich-e2e... --frozen-lockfile"
[tasks.build]
dir = "{{ config_root }}"
@ -40,18 +40,19 @@ run = "tsc --noEmit"
[tasks.ci-setup]
depends = [
"//:sdk:install",
"//:sdk:build",
"//packages/cli:install",
"//packages/cli:build",
run = [
{ task = "//:sdk:install" },
{ task = "//:sdk:build" },
{ task = "//packages/cli:install" },
{ task = "//packages/cli:build" },
{ task = ":install" },
]
run = { task = ":install" }
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = "//:sdk:install" },
{ task = "//:sdk:build" },
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },

View file

@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "3.0.0-rc.2",
"version": "3.1.0",
"description": "",
"main": "index.js",
"type": "module",
@ -32,7 +32,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.13.2",
"@types/node": "^24.13.3",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0",
@ -40,7 +40,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-unicorn": "^72.0.0",
"exiftool-vendored": "^35.0.0",
"globals": "^17.0.0",
"luxon": "^3.4.4",
@ -51,7 +51,8 @@
"sharp": "^0.34.5",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^6.0.0",
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-eslint": "^8.28.0",
"utimes": "^5.2.1",
"vitest": "^4.0.0"

View file

@ -1,651 +0,0 @@
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/duplicates', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[user1, user2] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
]);
});
beforeEach(async () => {
// Reset assets, albums, tags, and stacks between tests to ensure clean state for repeated test runs
// Note: We don't reset users since they're set up once in beforeAll
// Stack must be reset before asset due to foreign key constraint
await utils.resetDatabase(['stack', 'asset', 'album', 'tag']);
});
describe('GET /duplicates', () => {
it('should return empty array when no duplicates', async () => {
const { status, body } = await request(app)
.get('/duplicates')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([]);
});
it('should return duplicate groups with suggestedKeepAssetIds', async () => {
// Create assets with different file sizes for duplicate detection
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Manually set duplicateId on both assets to create a duplicate group
const duplicateId = '00000000-0000-4000-8000-000000000001';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.get('/duplicates')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
duplicateId,
assets: expect.arrayContaining([
expect.objectContaining({ id: asset1.id }),
expect.objectContaining({ id: asset2.id }),
]),
suggestedKeepAssetIds: expect.any(Array),
},
]);
expect(body[0].suggestedKeepAssetIds.length).toBe(1);
});
});
describe('POST /duplicates/resolve', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/duplicates/resolve')
.send({
groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }],
});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return failure for non-existent duplicate group', async () => {
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }],
});
expect(status).toBe(200);
expect(body).toEqual({
status: 'COMPLETED',
results: [
{
duplicateId: uuidDto.dummy,
status: 'FAILED',
reason: expect.stringContaining('not found or access denied'),
},
],
});
});
it('should resolve duplicate group with keepers', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000002';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body).toEqual({
status: 'COMPLETED',
results: [
{
duplicateId,
status: 'SUCCESS',
},
],
});
// Verify side effects: duplicateId cleared on kept asset
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.duplicateId).toBeNull();
// Verify side effects: trashed asset is trashed and duplicateId cleared
const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id);
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.duplicateId).toBeNull();
});
it('should reject when keepAssetIds and trashAssetIds overlap', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000003';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset1.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('FAILED');
expect(body.results[0].reason).toContain('disjoint');
});
it('should require keepAssetIds when partially trashing', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000004';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('FAILED');
expect(body.results[0].reason).toContain('must cover all assets');
});
it('should reject partial resolution (not all assets covered)', async () => {
const [asset1, asset2, asset3] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000010';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset3.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('FAILED');
expect(body.results[0].reason).toContain('must cover all assets');
});
it('should reject asset not in duplicate group', async () => {
const [asset1, asset2, outsideAsset] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000011';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [outsideAsset.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('FAILED');
expect(body.results[0].reason).toContain('not a member of duplicate group');
});
it('should allow trash-all without keepers', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000012';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id, asset2.id] }],
});
expect(status).toBe(200);
expect(body).toEqual({
status: 'COMPLETED',
results: [
{
duplicateId,
status: 'SUCCESS',
},
],
});
// Verify both assets are trashed
const [asset1Info, asset2Info] = await Promise.all([
utils.getAssetInfo(user1.accessToken, asset1.id),
utils.getAssetInfo(user1.accessToken, asset2.id),
]);
expect(asset1Info.isTrashed).toBe(true);
expect(asset1Info.duplicateId).toBeNull();
expect(asset2Info.isTrashed).toBe(true);
expect(asset2Info.duplicateId).toBeNull();
});
it('should reject cross-user duplicate group access', async () => {
const asset1 = await utils.createAsset(user1.accessToken);
const asset2 = await utils.createAsset(user2.accessToken);
const duplicateId = '00000000-0000-4000-8000-000000000013';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user2.accessToken, asset2.id, duplicateId);
// User1 tries to resolve a group containing user2's asset
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('FAILED');
expect(body.results[0].reason).toContain('not a member of duplicate group');
});
it('should synchronize favorites when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Mark one asset as favorite
await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset2.id], isFavorite: true });
const duplicateId = '00000000-0000-4000-8000-000000000020';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify favorite was synchronized to keeper
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.isFavorite).toBe(true);
expect(keptAsset.duplicateId).toBeNull();
});
it('should synchronize visibility when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Archive one asset
await utils.archiveAssets(user1.accessToken, [asset2.id]);
const duplicateId = '00000000-0000-4000-8000-000000000021';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify visibility was synchronized to keeper
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.visibility).toBe('archive');
expect(keptAsset.duplicateId).toBeNull();
});
it('should synchronize rating when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Set rating on one asset
await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset2.id], rating: 5 });
const duplicateId = '00000000-0000-4000-8000-000000000022';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify rating was synchronized to keeper
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.exifInfo?.rating).toBe(5);
expect(keptAsset.duplicateId).toBeNull();
});
it('should synchronize description when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Set description on one asset
await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset2.id], description: 'Test description for duplicate' });
const duplicateId = '00000000-0000-4000-8000-000000000023';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify description was synchronized to keeper
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.exifInfo?.description).toBe('Test description for duplicate');
expect(keptAsset.duplicateId).toBeNull();
});
it('should synchronize location when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Set location on one asset
await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset2.id], latitude: 40.7128, longitude: -74.006 });
const duplicateId = '00000000-0000-4000-8000-000000000024';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify location was synchronized to keeper
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.exifInfo?.latitude).toBe(40.7128);
expect(keptAsset.exifInfo?.longitude).toBe(-74.006);
expect(keptAsset.duplicateId).toBeNull();
});
it('should synchronize albums when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Create albums and add assets to different albums
const album1 = await utils.createAlbum(user1.accessToken, {
albumName: 'Album 1',
assetIds: [asset1.id],
});
const album2 = await utils.createAlbum(user1.accessToken, {
albumName: 'Album 2',
assetIds: [asset2.id],
});
const duplicateId = '00000000-0000-4000-8000-000000000025';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify keeper is now in both albums
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.duplicateId).toBeNull();
// Check albums directly
const { status: album1Status, body: album1Body } = await request(app)
.get(`/albums/${album1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
const { status: album2Status, body: album2Body } = await request(app)
.get(`/albums/${album2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(album1Status).toBe(200);
expect(album2Status).toBe(200);
expect(album1Body.assets.map((a: any) => a.id)).toContain(asset1.id);
expect(album2Body.assets.map((a: any) => a.id)).toContain(asset1.id);
});
it('should synchronize tags when enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// Wait for metadata extraction to complete before adding tags
// Otherwise, metadata jobs will race and overwrite our tags
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
// Create tags and tag assets differently
const tags = await utils.upsertTags(user1.accessToken, ['tag1', 'tag2']);
await utils.tagAssets(user1.accessToken, tags[0].id, [asset1.id]);
await utils.tagAssets(user1.accessToken, tags[1].id, [asset2.id]);
const duplicateId = '00000000-0000-4000-8000-000000000026';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify keeper has both tags
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(keptAsset.duplicateId).toBeNull();
expect(keptAsset.tags).toBeDefined();
const tagIds = keptAsset.tags?.map((t) => t.id) || [];
expect(tagIds).toContain(tags[0].id);
expect(tagIds).toContain(tags[1].id);
});
it('should handle batch resolve with mixed success and failure', async () => {
// Create first group that will succeed
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId1 = '00000000-0000-4000-8000-000000000027';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId1);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId1);
// Create second group with non-existent duplicate ID (will fail)
const fakeId = '00000000-0000-4000-8000-000000000099';
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [
{ duplicateId: duplicateId1, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] },
{ duplicateId: fakeId, keepAssetIds: [], trashAssetIds: [] },
],
});
expect(status).toBe(200);
expect(body.status).toBe('COMPLETED');
expect(body.results).toHaveLength(2);
// First group should succeed
expect(body.results[0].duplicateId).toBe(duplicateId1);
expect(body.results[0].status).toBe('SUCCESS');
// Second group should fail
expect(body.results[1].duplicateId).toBe(fakeId);
expect(body.results[1].status).toBe('FAILED');
expect(body.results[1].reason).toContain('not found or access denied');
// Verify first group was actually resolved despite second failure
const asset1Info = await utils.getAssetInfo(user1.accessToken, asset1.id);
expect(asset1Info.duplicateId).toBeNull();
const asset2Info = await utils.getAssetInfo(user1.accessToken, asset2.id);
expect(asset2Info.isTrashed).toBe(true);
});
it('should trash assets when trash is enabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000028';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
// Ensure trash is enabled (default)
const config = await utils.getSystemConfig(admin.accessToken);
expect(config.trash.enabled).toBe(true);
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Verify asset is trashed (not deleted)
const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id);
expect(trashedAsset.isTrashed).toBe(true);
});
it('should delete assets when trash is disabled', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const duplicateId = '00000000-0000-4000-8000-000000000029';
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
// Disable trash
await request(app)
.put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
trash: { enabled: false, days: 30 },
});
const { status, body } = await request(app)
.post('/duplicates/resolve')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
});
expect(status).toBe(200);
expect(body.results[0].status).toBe('SUCCESS');
// Asset should be marked as deleted (force delete)
const { status: getStatus } = await request(app)
.get(`/assets/${asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
// Asset should still be accessible (soft deleted) but marked as deleted
expect(getStatus).toBe(200);
// Re-enable trash for other tests
await utils.resetAdminConfig(admin.accessToken);
});
});
});

View file

@ -1,9 +1,7 @@
export const uuidDto = {
invalid: 'invalid-uuid',
// valid uuid v4
notFound: '00000000-0000-4000-a000-000000000000',
dummy: '00000000-0000-4000-a000-000000000001',
dummy2: '00000000-0000-4000-a000-000000000002',
};
const adminLoginDto = {
@ -57,22 +55,6 @@ export const createUserDto = {
};
export const userDto = {
admin: {
name: signupDto.admin.name,
email: signupDto.admin.email,
password: signupDto.admin.password,
storageLabel: 'admin',
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
user1: {
name: createUserDto.user1.name,
email: createUserDto.user1.email,

View file

@ -38,46 +38,8 @@ export const errorDto = {
incorrectLogin: {
message: 'Incorrect email or password',
},
alreadyHasAdmin: {
message: 'The server already has an admin',
},
};
export const signupResponseDto = {
admin: {
avatarColor: expect.any(String),
id: expect.any(String),
name: 'Immich Admin',
email: 'admin@immich.cloud',
storageLabel: 'admin',
profileImagePath: '',
// why? lol
shouldChangePassword: true,
isAdmin: true,
createdAt: expect.any(String),
updatedAt: expect.any(String),
deletedAt: null,
oauthId: '',
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',
license: null,
profileChangedAt: expect.any(String),
},
};
export const loginResponseDto = {
admin: {
accessToken: expect.any(String),
name: 'Immich Admin',
isAdmin: true,
isOnboarded: false,
profileImagePath: '',
shouldChangePassword: true,
userEmail: 'admin@immich.cloud',
userId: expect.any(String),
},
};
export const deviceDto = {
current: {
id: expect.any(String),

View file

@ -108,7 +108,7 @@ describe('/admin/database-backups', () => {
const { status, body } = await request(app).post('/admin/database-backups/start-restore').send();
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('The server already has an admin'));
expect(body).toEqual(errorDto.badRequest('Admin setup is not available'));
});
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
@ -118,7 +118,7 @@ describe('/admin/database-backups', () => {
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(
@ -224,7 +224,7 @@ describe('/admin/database-backups', () => {
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(
@ -295,7 +295,7 @@ describe('/admin/database-backups', () => {
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
await expect
.poll(

View file

@ -85,7 +85,7 @@ describe('/admin/maintenance', () => {
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
cookie = headers['set-cookie'][0].split(';', 1)[0];
expect(cookie).toEqual(
expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/),
);
@ -149,7 +149,7 @@ describe('/admin/maintenance', () => {
const { status, body } = await request(app)
.post('/admin/maintenance/login')
.send({
token: cookie!.split('=')[1].trim(),
token: cookie!.split('=', 2)[1].trim(),
});
expect(status).toBe(201);
expect(body).toEqual(

View file

@ -27,7 +27,7 @@ test.describe('Maintenance', () => {
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
const setCookie = await utils.enterMaintenance(admin.accessToken);
const cookie = setCookie
?.map((cookie) => cookie.split(';')[0].split('='))
?.map((cookie) => cookie.split(';', 1)[0].split('='))
?.find(([name]) => name === 'immich_maintenance_token');
expect(cookie).toBeTruthy();

View file

@ -120,6 +120,7 @@ describe('/albums', () => {
}),
]);
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
[user2Albums[0]] = await Promise.all([
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
@ -796,5 +797,22 @@ describe('/albums', () => {
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.share access'));
});
it('should not allow an editor to change the role of an owner', async () => {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
});
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Editor);
const { status, body } = await request(app)
.put(`/albums/${album.id}/user/${user1.userId}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ role: AlbumUserRole.Editor });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('User is owner'));
});
});
});

View file

@ -781,7 +781,7 @@ describe('/asset', () => {
exifImageWidth: 4032,
exifImageHeight: 3024,
latitude: 41.2203,
longitude: -96.071_625,
longitude: -96.071625,
make: 'Apple',
model: 'iPhone 7',
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
@ -973,9 +973,9 @@ describe('/asset', () => {
fileSizeInByte: 31_175_472,
focalLength: 18.3,
iso: 100,
latitude: 36.613_24,
latitude: 36.61324,
lensModel: '18.3mm F2.8',
longitude: -121.897_85,
longitude: -121.89785,
make: 'RICOH IMAGING COMPANY, LTD.',
model: 'RICOH GR III',
orientation: '1',

View file

@ -75,7 +75,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
lon: expect.closeTo(-108.400968),
state: 'Colorado',
},
{
@ -83,7 +83,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
lon: expect.closeTo(-96.071625),
state: 'Nebraska',
},
]);
@ -123,7 +123,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
lon: expect.closeTo(-108.400968),
state: 'Colorado',
},
{
@ -131,7 +131,7 @@ describe('/map', () => {
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
lon: expect.closeTo(-96.071625),
state: 'Nebraska',
},
]);
@ -188,20 +188,20 @@ describe('/map', () => {
const reverseGeocodeTestCases = [
{
name: 'Vaucluse',
lat: -33.858_977_058_663_13,
lon: 151.278_490_730_270_48,
lat: -33.85897705866313,
lon: 151.27849073027048,
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
},
{
name: 'Ravenhall',
lat: -37.765_732_399_174_75,
lon: 144.752_453_164_883_3,
lat: -37.76573239917475,
lon: 144.7524531648833,
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
},
{
name: 'Scarborough',
lat: -31.894_346_156_789_997,
lon: 115.757_617_103_904_64,
lat: -31.894346156789997,
lon: 115.75761710390464,
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
},
];

View file

@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
});
// login
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
const response1 = await redirect(url.replace(authServer.internal, () => authServer.external));
const response2 = await request(authServer.external + response1.location)
.post('')
.set('Cookie', response1.cookies)

View file

@ -1,6 +1,5 @@
import { LoginResponseDto, createPartner } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@ -29,13 +28,6 @@ describe('/partners', () => {
});
describe('GET /partners', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/partners');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get all partners shared by user', async () => {
const { status, body } = await request(app)
.get('/partners')
@ -58,13 +50,6 @@ describe('/partners', () => {
});
describe('POST /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/partners/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should share with new partner', async () => {
const { status, body } = await request(app)
.post(`/partners/${user3.userId}`)
@ -85,13 +70,6 @@ describe('/partners', () => {
});
describe('PUT /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/partners/${user2.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update partner', async () => {
const { status, body } = await request(app)
.put(`/partners/${user2.userId}`)
@ -104,13 +82,6 @@ describe('/partners', () => {
});
describe('DELETE /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/partners/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should delete partner', async () => {
const { status } = await request(app)
.delete(`/partners/${user3.userId}`)

View file

@ -4,6 +4,7 @@ import {
AssetVisibility,
deleteAssets,
LoginResponseDto,
SharedLinkType,
updateAsset,
} from '@immich/sdk';
import { DateTime } from 'luxon';
@ -86,23 +87,23 @@ describe('/search', () => {
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
const coordinates = [
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
{ latitude: 48.85341, longitude: 2.3488 }, // paris
{ latitude: 35.6895, longitude: 139.69171 }, // tokyo
{ latitude: 52.52437, longitude: 13.41053 }, // berlin
{ latitude: 1.3146631, longitude: 103.8454093 }, // singapore
{ latitude: 41.01384, longitude: 28.94966 }, // istanbul
{ latitude: 5.55602, longitude: -0.1969 }, // accra
{ latitude: 37.5442706, longitude: -4.7277528 }, // andalusia
{ latitude: 23.13302, longitude: -82.38304 }, // havana
{ latitude: 41.69411, longitude: 44.83368 }, // tbilisi
{ latitude: 31.22222, longitude: 121.45806 }, // shanghai
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
{ latitude: 40.71427, longitude: -74.00597 }, // new york
{ latitude: 47.04057, longitude: 9.06804 }, // glarus
{ latitude: 32.77152, longitude: -89.11673 }, // philadelphia
{ latitude: 31.63416, longitude: -7.99994 }, // marrakesh
{ latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge
{ latitude: 59.93863, longitude: 30.31413 }, // st. petersburg
{ latitude: 0, longitude: 0 }, // null island
];
@ -111,7 +112,7 @@ describe('/search', () => {
);
await Promise.all(updates);
for (const [i] of coordinates.entries()) {
for (const i of coordinates.keys()) {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
}
@ -357,6 +358,32 @@ describe('/search', () => {
expect(body.assets.items).toHaveLength(assets.length);
});
}
it('should reject shared link access without an album filter', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'foo' });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app).post(`/search/metadata?key=${sharedLink.key}`).send({});
expect(status).toBe(400);
expect(body).toEqual({ message: 'Shared link access is only allowed in combination with an albumIds filter' });
});
it('should allow shared link access for albums', async () => {
const asset = await utils.createAsset(admin.accessToken);
const album = await utils.createAlbum(admin.accessToken, { albumName: 'foo', assetIds: [asset.id] });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app)
.post(`/search/metadata?key=${sharedLink.key}`)
.send({ albumIds: [album.id] });
expect(status).toBe(200);
expect(body.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
});
});
describe('POST /search/random', () => {

View file

@ -22,12 +22,6 @@ describe('/server', () => {
});
describe('GET /server/about', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/about');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return about information', async () => {
const { status, body } = await request(app)
.get('/server/about')
@ -56,12 +50,6 @@ describe('/server', () => {
});
describe('GET /server/storage', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/storage');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return the disk information', async () => {
const { status, body } = await request(app)
.get('/server/storage')
@ -91,12 +79,14 @@ describe('/server', () => {
it('should respond with the server version', async () => {
const { status, body } = await request(app).get('/server/version');
expect(status).toBe(200);
expect(body).toEqual({
major: expect.any(Number),
minor: expect.any(Number),
patch: expect.any(Number),
prerelease: expect.anything(),
});
expect(body).toEqual(
expect.objectContaining({
major: expect.any(Number),
minor: expect.any(Number),
patch: expect.any(Number),
}),
);
expect(Object.keys(body)).toEqual(expect.arrayContaining(['major', 'minor', 'patch', 'prerelease']));
});
});
@ -147,12 +137,6 @@ describe('/server', () => {
});
describe('GET /server/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/statistics')
@ -211,12 +195,6 @@ describe('/server', () => {
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/license')
@ -239,12 +217,6 @@ describe('/server', () => {
});
describe('DELETE /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.delete('/server/license')
@ -264,12 +236,6 @@ describe('/server', () => {
});
describe('PUT /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.put('/server/license')

View file

@ -15,12 +15,6 @@ describe('/sessions', () => {
});
describe('GET /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/sessions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@ -29,12 +23,6 @@ describe('/sessions', () => {
});
describe('DELETE /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/sessions`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout all devices (except the current one)', async () => {
for (let i = 0; i < 5; i++) {
await login({ loginCredentialDto: loginDto.admin });

View file

@ -137,13 +137,6 @@ describe('/shared-links', () => {
});
describe('GET /shared-links', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/shared-links');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get all shared links created by user', async () => {
const { status, body } = await request(app)
.get('/shared-links')
@ -200,12 +193,6 @@ describe('/shared-links', () => {
});
describe('GET /shared-links/me', () => {
it('should not require admin authentication', async () => {
const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403);
});
it('should get data for correct shared link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
@ -293,13 +280,6 @@ describe('/shared-links', () => {
});
describe('GET /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get shared link by id', async () => {
const { status, body } = await request(app)
.get(`/shared-links/${linkWithAlbum.id}`)
@ -326,26 +306,6 @@ describe('/shared-links', () => {
});
describe('POST /shared-links', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/shared-links')
.send({ type: SharedLinkType.Album, albumId: uuidDto.notFound });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a type and the correspondent asset/album id', async () => {
const { status, body } = await request(app)
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
);
});
it('should require an asset/album id', async () => {
const { status, body } = await request(app)
.post('/shared-links')
@ -353,17 +313,17 @@ describe('/shared-links', () => {
.send({ type: SharedLinkType.Album });
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'albumId is required for type ALBUM' }]));
});
it('should require a valid asset id', async () => {
const { status, body } = await request(app)
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
.send({ type: SharedLinkType.Individual, assetIds: [uuidDto.notFound] });
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
expect(body).toEqual(expect.objectContaining({ message: 'Not found or no asset.share access' }));
});
it('should create a shared link', async () => {
@ -383,15 +343,6 @@ describe('/shared-links', () => {
});
describe('PATCH /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/shared-links/${linkWithAlbum.id}`)
.send({ description: 'foo' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should fail if invalid link', async () => {
const { status, body } = await request(app)
.patch(`/shared-links/${uuidDto.notFound}`)
@ -474,13 +425,6 @@ describe('/shared-links', () => {
});
describe('DELETE /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should fail if invalid link', async () => {
const { status, body } = await request(app)
.delete(`/shared-links/${uuidDto.notFound}`)

View file

@ -1,5 +1,5 @@
import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
@ -25,42 +25,6 @@ describe('/stacks', () => {
});
describe('POST /stacks', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/stacks')
.send({ assetIds: [asset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require at least two assets', async () => {
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset.id] });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['assetIds', 0], message: 'Invalid UUID' },
{ path: ['assetIds', 1], message: 'Invalid UUID' },
]),
);
});
it('should require access', async () => {
const user2Asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app)

View file

@ -16,12 +16,6 @@ describe('/server-info', () => {
});
describe('POST /system-metadata/admin-onboarding', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.post('/system-metadata/admin-onboarding')
@ -47,12 +41,6 @@ describe('/server-info', () => {
});
describe('GET /system-metadata/reverse-geocoding-state', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/system-metadata/reverse-geocoding-state')

View file

@ -41,12 +41,6 @@ describe('/tags', () => {
});
describe('POST /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/tags').send({ name: 'TagA' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
@ -117,12 +111,6 @@ describe('/tags', () => {
});
describe('GET /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/tags');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).get('/tags').set('x-api-key', secret);
@ -169,12 +157,6 @@ describe('/tags', () => {
});
describe('PUT /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' });
@ -213,12 +195,6 @@ describe('/tags', () => {
});
describe('PUT /tags/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
@ -279,12 +255,6 @@ describe('/tags', () => {
});
describe('GET /tags/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -304,14 +274,6 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should get tag details', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -349,13 +311,6 @@ describe('/tags', () => {
});
describe('PUT /tags/:id', () => {
it('should require authentication', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(admin.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
@ -399,12 +354,6 @@ describe('/tags', () => {
});
describe('DELETE /tags/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -422,14 +371,6 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.missingPermission('tag.delete'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should delete a tag', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status } = await request(app)
@ -463,15 +404,6 @@ describe('/tags', () => {
});
describe('PUT /tags/:id/assets', () => {
it('should require authentication', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.send({ ids: [userAsset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -531,16 +463,6 @@ describe('/tags', () => {
});
describe('DELETE /tags/:id/assets', () => {
it('should require authentication', async () => {
const tagA = await create(admin.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.delete(`/tags/${tagA}/assets`)
.send({ ids: [userAsset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(

View file

@ -1,5 +1,6 @@
import {
LoginResponseDto,
Permission,
QueueName,
createStack,
deleteUserAdmin,
@ -45,12 +46,6 @@ describe('/admin/users', () => {
});
describe('GET /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/admin/users`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
@ -93,12 +88,6 @@ describe('/admin/users', () => {
});
describe('POST /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
@ -108,23 +97,6 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
['notify', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
it('should accept `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
@ -145,12 +117,6 @@ describe('/admin/users', () => {
});
describe('PUT /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
@ -159,22 +125,6 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
it('should allow a non-admin to become an admin', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
const { status, body } = await request(app)
@ -244,12 +194,6 @@ describe('/admin/users', () => {
});
describe('PUT /admin/users/:id/preferences', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update memories enabled', async () => {
const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
@ -281,12 +225,6 @@ describe('/admin/users', () => {
});
describe('DELETE /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
@ -358,13 +296,26 @@ describe('/admin/users', () => {
});
});
describe('POST /admin/users/:id/restore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
describe('GET /admin/users/:id/calendar-heatmap', () => {
it('should require admin permissions', async () => {
const { status, body } = await request(app)
.get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should require the AdminUserRead permission', async () => {
const { secret } = await utils.createApiKey(admin.accessToken, [Permission.UserRead]);
const { status, body } = await request(app)
.get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`)
.set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
});
describe('POST /admin/users/:id/restore', () => {
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users/${userToDelete.userId}/restore`)

View file

@ -1,34 +1,18 @@
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk';
import { LoginResponseDto, SharedLinkType, getMyUser, login } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
const userLicense = {
licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4',
activationKey:
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
};
describe('/users', () => {
let admin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
]);
await deleteUserAdmin(
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
});
describe('GET /users/me', () => {
@ -42,55 +26,9 @@ describe('/users', () => {
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get my user', async () => {
const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
quotaUsageInBytes: 0,
});
});
it('should get my user with license info', async () => {
const { status: licenseStatus } = await request(app)
.put(`/users/me/license`)
.send(userLicense)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(licenseStatus).toBe(200);
const { status, body } = await request(app)
.get(`/users/me`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: nonAdmin.userId,
email: nonAdmin.userEmail,
quotaUsageInBytes: 0,
license: userLicense,
});
});
});
describe('PUT /users/me', () => {
it('should update first and last name', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ name: 'Name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...before,
updatedAt: expect.any(String),
profileChangedAt: expect.any(String),
createdAt: expect.any(String),
name: 'Name',
});
});
/** @deprecated */
it('should allow a user to change their password (deprecated)', async () => {
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
@ -112,222 +50,5 @@ describe('/users', () => {
expect(token.accessToken).toBeDefined();
});
it('should not allow user to change to a taken email', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email is not available'));
});
it('should update my email', async () => {
const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'non-admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
email: 'non-admin@immich.cloud',
updatedAt: expect.anything(),
createdAt: expect.anything(),
profileChangedAt: expect.anything(),
});
});
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ avatarColor: 'blue' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'blue' });
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'blue' });
});
});
describe('PUT /users/me/preferences', () => {
it('should update memories enabled', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ memories: { enabled: false } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ memories: { enabled: false } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
it('should require an integer for download archive size', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567.89 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
]),
);
});
it('should update download archive size', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
});
it('should require a boolean for download include embedded videos', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { includeEmbeddedVideos: 1_234_567.89 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
]),
);
});
it('should update download include embedded videos', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { includeEmbeddedVideos: true } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
});
it('should update minimum face count to display people', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ people: { minimumFaces: 3 } });
const { status, body } = await request(app)
.put('/users/me/preferences')
.send({ people: { minimumFaces: 2 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ people: { minimumFaces: 2 } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ people: { minimumFaces: 2 } });
});
});
describe('GET /users/:id', () => {
it('should get the user', async () => {
const { status, body } = await request(app)
.get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
expect(body).not.toMatchObject({
shouldChangePassword: expect.anything(),
storageLabel: expect.anything(),
});
});
});
describe('GET /server/license', () => {
it('should return the user license', async () => {
await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send(userLicense);
const { status, body } = await request(app)
.get('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...userLicense,
activatedAt: expect.any(String),
});
});
});
describe('PUT /users/me/license', () => {
it('should set the user license', async () => {
const { status, body } = await request(app)
.put(`/users/me/license`)
.send(userLicense)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ ...userLicense, activatedAt: expect.any(String) });
expect(status).toBe(200);
expect(body).toEqual({ ...userLicense, activatedAt: expect.any(String) });
const { body: licenseBody } = await request(app)
.get('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(licenseBody).toEqual({ ...userLicense, activatedAt: expect.any(String) });
});
it('should reject license not starting with IMCL-', async () => {
const { status, body } = await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
it('should reject license with invalid activation key', async () => {
const { status, body } = await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ licenseKey: userLicense.licenseKey, activationKey: `invalid${userLicense.activationKey}` });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
});
describe('DELETE /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(app).put(`/users/me/license`);
expect(status).toEqual(401);
});
it('should delete the user license', async () => {
const { status } = await request(app)
.delete(`/users/me/license`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(204);
});
});
});

View file

@ -31,7 +31,7 @@ describe(`immich login`, () => {
it('should login and save auth.yml with 600', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285/api',
'Logged in as admin@immich.cloud',
@ -48,7 +48,7 @@ describe(`immich login`, () => {
it('should login without /api in the url', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285',
'Discovered API at http://127.0.0.1:2285/api',

View file

@ -119,7 +119,9 @@ describe(`immich upload`, () => {
const baseDir = `/tmp/upload/`;
const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
testPaths.map((filePath) => utils.createImageFile(filePath));
for (const filePath of testPaths) {
utils.createImageFile(filePath);
}
const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
@ -135,7 +137,9 @@ describe(`immich upload`, () => {
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(expectedCount);
testPaths.map((filePath) => utils.removeImageFile(filePath));
for (const filePath of testPaths) {
utils.removeImageFile(filePath);
}
});
}
});
@ -646,7 +650,7 @@ describe(`immich upload`, () => {
]);
expect(stdout).toBe('');
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '--skip-hash'`);
expect(exitCode).not.toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });

View file

@ -65,6 +65,10 @@ describe(`immich-admin`, () => {
child.stdout.on('data', (chunk) => {
data += chunk;
if (data.includes('Please choose a new password (optional)')) {
child.stdin.write('\n');
}
if (data.includes('Invalidate existing sessions?')) {
child.stdin.end('\n');
}
});

View file

@ -21,7 +21,7 @@ export const randomImageFromString = async (
let seedNumber = 0;
for (let i = 0; i < seed.length; i++) {
seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0);
seedNumber = seedNumber & seedNumber; // Convert to 32bit integer
seedNumber &= seedNumber; // Convert to 32bit integer
}
return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height });
};

Some files were not shown because too many files have changed in this diff Show more