mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d6131e490 | |||
| 10218fb900 |
@@ -51,14 +51,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore Gradle Cache
|
- name: Restore Gradle Cache
|
||||||
id: cache-gradle-restore
|
id: cache-gradle-restore
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
key: build-mobile-gradle-${{ runner.os }}-main
|
key: build-mobile-gradle-${{ runner.os }}-main
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -185,13 +185,13 @@ jobs:
|
|||||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -210,7 +210,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@2a37bc82462349c03a533b8b608bebbaf57b3e60 # v0.0.33
|
uses: oasdiff/oasdiff-action/breaking@748daafaf3aac877a36307f842a48d55db938ac8 # v0.0.31
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
needs: [get_body, should_run]
|
needs: [get_body, should_run]
|
||||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
|
image: ghcr.io/immich-app/mdq:main@sha256:4f9860d04c88f7f87861f8ee84bfeedaec15ed7ca5ca87bc7db44b036f81645f
|
||||||
outputs:
|
outputs:
|
||||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ 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
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
suffixes: '-rocm'
|
suffixes: '-rocm'
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -131,7 +131,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2
|
||||||
|
|
||||||
- name: Load parameters
|
- name: Load parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2
|
||||||
|
|
||||||
- name: Destroy Docs Subdomain
|
- name: Destroy Docs Subdomain
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Require PR to have a changelog label
|
- name: Require PR to have a changelog label
|
||||||
uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
|
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
mode: exactly
|
mode: exactly
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ needs.bump_version.outputs.version }}
|
tag_name: ${{ needs.bump_version.outputs.version }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|||||||
+210
-87
@@ -17,14 +17,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -197,7 +197,7 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -241,7 +241,7 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -279,7 +279,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -327,7 +327,7 @@ jobs:
|
|||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -373,7 +373,7 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -404,6 +404,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
actions: write
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
@@ -412,62 +413,93 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Setup pnpm
|
- name: Set up Docker Buildx
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Setup Node
|
- name: Compute server cache key
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
run: |
|
||||||
with:
|
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||||
node-version-file: './e2e/.nvmrc'
|
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||||
cache: 'pnpm'
|
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||||
- name: Run setup typescript-sdk
|
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
- name: Build Docker images from cache
|
||||||
working-directory: ./open-api/typescript-sdk
|
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Run setup web
|
|
||||||
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
|
||||||
working-directory: ./web
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Run setup cli
|
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
|
||||||
working-directory: ./cli
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Start Docker Compose
|
- name: Start Docker Compose
|
||||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
env:
|
run: docker compose --profile test run --rm e2e-runner pnpm test
|
||||||
VITEST_DISABLE_DOCKER_SETUP: true
|
|
||||||
run: pnpm test
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Run e2e tests (maintenance)
|
|
||||||
env:
|
|
||||||
VITEST_DISABLE_DOCKER_SETUP: true
|
|
||||||
run: pnpm test:maintenance
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Capture Docker logs
|
- name: Capture Docker logs
|
||||||
if: always()
|
if: always()
|
||||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
|
- name: Archive Docker logs
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: e2e-server-docker-logs-${{ matrix.runner }}
|
||||||
|
path: e2e/docker-compose-logs.txt
|
||||||
|
e2e-tests-server-maintenance:
|
||||||
|
name: End-to-End Tests (Server Maintenance)
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
steps:
|
||||||
|
- id: token
|
||||||
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: 'recursive'
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
- name: Compute server cache key
|
||||||
|
run: |
|
||||||
|
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||||
|
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||||
|
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||||
|
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||||
|
- name: Build Docker images from cache
|
||||||
|
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||||
|
- name: Start Docker Compose
|
||||||
|
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
- name: Run e2e tests (maintenance)
|
||||||
|
run: docker compose --profile test run --rm e2e-runner pnpm test:maintenance
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
- name: Capture Docker logs
|
||||||
|
if: always()
|
||||||
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
- name: Archive Docker logs
|
- name: Archive Docker logs
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: e2e-server-docker-logs-${{ matrix.runner }}
|
name: e2e-server-maintenance-docker-logs-${{ matrix.runner }}
|
||||||
path: e2e/docker-compose-logs.txt
|
path: e2e/docker-compose-logs.txt
|
||||||
e2e-tests-web:
|
e2e-tests-web:
|
||||||
name: End-to-End Tests (Web)
|
name: End-to-End Tests (Web)
|
||||||
@@ -476,6 +508,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
actions: write
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
@@ -484,42 +517,32 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Setup pnpm
|
- name: Set up Docker Buildx
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Setup Node
|
- name: Compute server cache key
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
run: |
|
||||||
with:
|
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||||
node-version-file: './e2e/.nvmrc'
|
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||||
cache: 'pnpm'
|
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||||
- name: Run setup typescript-sdk
|
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||||
run: pnpm install --frozen-lockfile && pnpm build
|
- name: Build Docker images from cache
|
||||||
working-directory: ./open-api/typescript-sdk
|
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||||
if: ${{ !cancelled() }}
|
- name: Start Docker Compose
|
||||||
- name: Install dependencies
|
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: pnpm exec playwright install chromium --only-shell
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
- name: Docker build
|
|
||||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
env:
|
run: docker compose --profile test run --rm e2e-runner pnpm test:web
|
||||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
|
||||||
run: pnpm test:web
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive e2e test (web) results
|
- name: Archive e2e test (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
@@ -527,10 +550,57 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: e2e-web-test-results-${{ matrix.runner }}
|
name: e2e-web-test-results-${{ matrix.runner }}
|
||||||
path: e2e/playwright-report/
|
path: e2e/playwright-report/
|
||||||
|
- name: Capture Docker logs
|
||||||
|
if: always()
|
||||||
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
|
- name: Archive Docker logs
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: e2e-web-docker-logs-${{ matrix.runner }}
|
||||||
|
path: e2e/docker-compose-logs.txt
|
||||||
|
e2e-tests-web-ui:
|
||||||
|
name: End-to-End Tests (Web UI)
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./e2e
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
steps:
|
||||||
|
- id: token
|
||||||
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: 'recursive'
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
- name: Compute server cache key
|
||||||
|
run: |
|
||||||
|
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||||
|
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||||
|
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||||
|
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||||
|
- name: Build Docker images from cache
|
||||||
|
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||||
|
- name: Start Docker Compose
|
||||||
|
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
- name: Run ui tests (web)
|
- name: Run ui tests (web)
|
||||||
env:
|
run: docker compose --profile test run --rm e2e-runner pnpm test:web:ui
|
||||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
|
||||||
run: pnpm test:web:ui
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive ui test (web) results
|
- name: Archive ui test (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
@@ -538,10 +608,57 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: e2e-ui-test-results-${{ matrix.runner }}
|
name: e2e-ui-test-results-${{ matrix.runner }}
|
||||||
path: e2e/playwright-report/
|
path: e2e/playwright-report/
|
||||||
|
- name: Capture Docker logs
|
||||||
|
if: always()
|
||||||
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
|
- name: Archive Docker logs
|
||||||
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: e2e-web-ui-docker-logs-${{ matrix.runner }}
|
||||||
|
path: e2e/docker-compose-logs.txt
|
||||||
|
e2e-tests-web-maintenance:
|
||||||
|
name: End-to-End Tests (Web Maintenance)
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./e2e
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
|
steps:
|
||||||
|
- id: token
|
||||||
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
submodules: 'recursive'
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
- name: Compute server cache key
|
||||||
|
run: |
|
||||||
|
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||||
|
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||||
|
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||||
|
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||||
|
- name: Build Docker images from cache
|
||||||
|
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||||
|
- name: Start Docker Compose
|
||||||
|
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
- name: Run maintenance tests
|
- name: Run maintenance tests
|
||||||
env:
|
run: docker compose --profile test run --rm e2e-runner pnpm test:web:maintenance
|
||||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
|
||||||
run: pnpm test:web:maintenance
|
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive maintenance tests (web) results
|
- name: Archive maintenance tests (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
@@ -552,16 +669,22 @@ jobs:
|
|||||||
- name: Capture Docker logs
|
- name: Capture Docker logs
|
||||||
if: always()
|
if: always()
|
||||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
working-directory: ./e2e
|
|
||||||
- name: Archive Docker logs
|
- name: Archive Docker logs
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: e2e-web-docker-logs-${{ matrix.runner }}
|
name: e2e-web-maintenance-docker-logs-${{ matrix.runner }}
|
||||||
path: e2e/docker-compose-logs.txt
|
path: e2e/docker-compose-logs.txt
|
||||||
success-check-e2e:
|
success-check-e2e:
|
||||||
name: End-to-End Tests Success
|
name: End-to-End Tests Success
|
||||||
needs: [e2e-tests-server-cli, e2e-tests-web]
|
needs:
|
||||||
|
[
|
||||||
|
e2e-tests-server-cli,
|
||||||
|
e2e-tests-server-maintenance,
|
||||||
|
e2e-tests-web,
|
||||||
|
e2e-tests-web-ui,
|
||||||
|
e2e-tests-web-maintenance,
|
||||||
|
]
|
||||||
permissions: {}
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
@@ -578,7 +701,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -588,7 +711,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -610,7 +733,7 @@ jobs:
|
|||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -620,7 +743,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -650,7 +773,7 @@ jobs:
|
|||||||
working-directory: ./.github
|
working-directory: ./.github
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -680,7 +803,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -701,7 +824,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -763,7 +886,7 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ e2e-update:
|
|||||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
e2e-down:
|
e2e-down:
|
||||||
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
docker compose -f ./e2e/docker-compose.yml --profile test down --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
@@ -101,10 +101,30 @@ check-web:
|
|||||||
pnpm --filter immich-web run check:svelte
|
pnpm --filter immich-web run check:svelte
|
||||||
test-%:
|
test-%:
|
||||||
pnpm --filter $(call map-package,$*) run test
|
pnpm --filter $(call map-package,$*) run test
|
||||||
test-e2e:
|
test-e2e: build-e2e test-e2e-server test-e2e-server-maintenance test-e2e-web test-e2e-web-ui test-e2e-web-maintenance
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
|
||||||
pnpm --filter immich-e2e run test
|
test-e2e-server:
|
||||||
pnpm --filter immich-e2e run test:web
|
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test
|
||||||
|
|
||||||
|
test-e2e-server-maintenance:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:maintenance
|
||||||
|
|
||||||
|
test-e2e-web:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web
|
||||||
|
|
||||||
|
test-e2e-web-ui:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web:ui
|
||||||
|
|
||||||
|
test-e2e-web-maintenance:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web:maintenance
|
||||||
|
|
||||||
|
build-e2e:
|
||||||
|
docker compose -f ./e2e/docker-compose.yml --profile test build
|
||||||
test-medium:
|
test-medium:
|
||||||
docker run \
|
docker run \
|
||||||
--rm \
|
--rm \
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const setup = async () => {
|
|||||||
const redirectUris = [
|
const redirectUris = [
|
||||||
'http://127.0.0.1:2285/auth/login',
|
'http://127.0.0.1:2285/auth/login',
|
||||||
'https://photos.immich.app/oauth/mobile-redirect',
|
'https://photos.immich.app/oauth/mobile-redirect',
|
||||||
|
...(process.env.EXTRA_REDIRECT_URIS?.split(',').filter(Boolean) ?? []),
|
||||||
];
|
];
|
||||||
const port = 2286;
|
const port = 2286;
|
||||||
const host = '0.0.0.0';
|
const host = '0.0.0.0';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/e2e-auth-server",
|
"name": "@immich/e2e-auth-server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "auth-server.ts",
|
"main": "auth-server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
docker.io \
|
||||||
|
unzip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .pnpmfile.cjs ./
|
||||||
|
COPY open-api/typescript-sdk/package.json open-api/typescript-sdk/
|
||||||
|
COPY cli/package.json cli/
|
||||||
|
COPY web/package.json web/
|
||||||
|
COPY e2e/package.json e2e/
|
||||||
|
COPY e2e-auth-server/package.json e2e-auth-server/
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY open-api/typescript-sdk/ open-api/typescript-sdk/
|
||||||
|
RUN pnpm --filter @immich/sdk build
|
||||||
|
|
||||||
|
COPY cli/ cli/
|
||||||
|
RUN pnpm --filter @immich/cli build && ln -s /app/cli/bin/immich /app/cli/node_modules/.bin/immich
|
||||||
|
|
||||||
|
COPY web/svelte.config.js web/vite.config.ts web/tsconfig.json web/
|
||||||
|
COPY web/src/ web/src/
|
||||||
|
COPY web/static/ web/static/
|
||||||
|
RUN pnpm --filter immich-web exec svelte-kit sync
|
||||||
|
|
||||||
|
COPY e2e/ e2e/
|
||||||
|
COPY e2e-auth-server/ e2e-auth-server/
|
||||||
|
|
||||||
|
RUN pnpm --filter immich-e2e exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
WORKDIR /app/e2e
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.vscode/
|
||||||
|
.github/
|
||||||
|
.git/
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
**/node_modules/
|
||||||
|
**/.pnpm-store/
|
||||||
|
**/dist/
|
||||||
|
**/coverage/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
design/
|
||||||
|
docker/
|
||||||
|
docs/
|
||||||
|
fastlane/
|
||||||
|
machine-learning/
|
||||||
|
misc/
|
||||||
|
mobile/
|
||||||
|
server/
|
||||||
|
plugins/
|
||||||
|
i18n/
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: immich-e2e
|
||||||
|
|
||||||
|
services:
|
||||||
|
e2e-auth-server:
|
||||||
|
build:
|
||||||
|
cache_from:
|
||||||
|
- type=gha,scope=e2e-auth-${RUNNER_ARCH:-X64}
|
||||||
|
cache_to:
|
||||||
|
- type=gha,mode=max,scope=e2e-auth-${RUNNER_ARCH:-X64}
|
||||||
|
|
||||||
|
e2e-runner:
|
||||||
|
build:
|
||||||
|
cache_from:
|
||||||
|
- type=gha,scope=e2e-runner-${RUNNER_ARCH:-X64}
|
||||||
|
cache_to:
|
||||||
|
- type=gha,mode=max,scope=e2e-runner-${RUNNER_ARCH:-X64}
|
||||||
+29
-2
@@ -5,6 +5,8 @@ services:
|
|||||||
container_name: immich-e2e-auth-server
|
container_name: immich-e2e-auth-server
|
||||||
build:
|
build:
|
||||||
context: ../e2e-auth-server
|
context: ../e2e-auth-server
|
||||||
|
environment:
|
||||||
|
EXTRA_REDIRECT_URIS: http://immich-server:2285/auth/login
|
||||||
ports:
|
ports:
|
||||||
- 2286:2286
|
- 2286:2286
|
||||||
|
|
||||||
@@ -15,8 +17,7 @@ services:
|
|||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
cache_from:
|
cache_from:
|
||||||
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:linux-amd64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main
|
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:${SERVER_CACHE_KEY:-linux-amd64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main}
|
||||||
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:linux-arm64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main
|
|
||||||
args:
|
args:
|
||||||
- BUILD_ID=1234567890
|
- BUILD_ID=1234567890
|
||||||
- BUILD_IMAGE=e2e
|
- BUILD_IMAGE=e2e
|
||||||
@@ -65,3 +66,29 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
retries: 30
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
|
e2e-runner:
|
||||||
|
container_name: immich-e2e-runner
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: e2e/Dockerfile.playwright
|
||||||
|
network_mode: 'service:immich-server'
|
||||||
|
shm_size: 1gb
|
||||||
|
environment:
|
||||||
|
PLAYWRIGHT_DB_HOST: database
|
||||||
|
PLAYWRIGHT_DB_PORT: '5432'
|
||||||
|
PLAYWRIGHT_DISABLE_WEBSERVER: 'true'
|
||||||
|
PLAYWRIGHT_AUTH_SERVER_URL: http://e2e-auth-server:2286
|
||||||
|
VITEST_DISABLE_DOCKER_SETUP: 'true'
|
||||||
|
CI: '${CI:-}'
|
||||||
|
volumes:
|
||||||
|
- ./test-assets:/app/e2e/test-assets
|
||||||
|
- ./playwright-report:/app/e2e/playwright-report
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
depends_on:
|
||||||
|
immich-server:
|
||||||
|
condition: service_started
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
profiles:
|
||||||
|
- test
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ dotenv.config({ quiet: true, path: resolve(import.meta.dirname, '.env') });
|
|||||||
|
|
||||||
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
|
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
|
||||||
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
|
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
|
||||||
|
export const playwrightDbPort = process.env.PLAYWRIGHT_DB_PORT ?? '5435';
|
||||||
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
|
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
|
||||||
export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
|
export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
|
||||||
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
|
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
|
||||||
@@ -43,7 +44,7 @@ const config: PlaywrightTestConfig = {
|
|||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
testDir: './src/ui/specs',
|
testDir: './src/ui/specs',
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
workers: process.env.CI ? 3 : Math.min(10, Math.max(1, Math.round(cpus().length * 0.75) - 1)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'maintenance',
|
name: 'maintenance',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
const authServer = {
|
const authServer = {
|
||||||
internal: 'http://e2e-auth-server:2286',
|
internal: 'http://e2e-auth-server:2286',
|
||||||
external: 'http://127.0.0.1:2286',
|
external: process.env.PLAYWRIGHT_AUTH_SERVER_URL ?? 'http://127.0.0.1:2286',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
|
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe('/shared-links', () => {
|
|||||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.header['content-type']).toContain('text/html');
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
|
expect(resp.text).toContain(`<meta property="og:image" content="${baseUrl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
|
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Permission } from '@immich/sdk';
|
import { Permission } from '@immich/sdk';
|
||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { app, immichCli, utils } from 'src/utils';
|
import { app, baseUrl, immichCli, utils } from 'src/utils';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich login`, () => {
|
describe(`immich login`, () => {
|
||||||
@@ -33,7 +33,7 @@ describe(`immich login`, () => {
|
|||||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
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([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2285/api',
|
`Logging in to ${baseUrl}/api`,
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
'Wrote auth info to /tmp/immich/auth.yml',
|
'Wrote auth info to /tmp/immich/auth.yml',
|
||||||
]);
|
]);
|
||||||
@@ -50,8 +50,8 @@ describe(`immich login`, () => {
|
|||||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
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([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2285',
|
`Logging in to ${baseUrl}`,
|
||||||
'Discovered API at http://127.0.0.1:2285/api',
|
`Discovered API at ${baseUrl}/api`,
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
'Wrote auth info to /tmp/immich/auth.yml',
|
'Wrote auth info to /tmp/immich/auth.yml',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { immichCli, utils } from 'src/utils';
|
import { baseUrl, immichCli, utils } from 'src/utils';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich server-info`, () => {
|
describe(`immich server-info`, () => {
|
||||||
@@ -12,7 +12,7 @@ describe(`immich server-info`, () => {
|
|||||||
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
expect.stringContaining('Server Info (via admin@immich.cloud'),
|
expect.stringContaining('Server Info (via admin@immich.cloud'),
|
||||||
' Url: http://127.0.0.1:2285/api',
|
` Url: ${baseUrl}/api`,
|
||||||
expect.stringContaining('Version:'),
|
expect.stringContaining('Version:'),
|
||||||
' Formats:',
|
' Formats:',
|
||||||
expect.stringContaining('Images:'),
|
expect.stringContaining('Images:'),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { lookup } from 'node:dns/promises';
|
||||||
|
import { playwrightHost } from 'playwright.config';
|
||||||
import { utils } from 'src/utils';
|
import { utils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Websocket', () => {
|
test.describe('Websocket', () => {
|
||||||
@@ -12,14 +14,28 @@ test.describe('Websocket', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('connects using ipv4', async ({ page, context }) => {
|
test('connects using ipv4', async ({ page, context }) => {
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
const { address: ipv4 } = await lookup(playwrightHost, 4);
|
||||||
await page.goto('http://127.0.0.1:2285/');
|
await utils.setAuthCookies(context, admin.accessToken, ipv4);
|
||||||
|
await page.goto(`http://${ipv4}:2285/`);
|
||||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('connects using ipv6', async ({ page, context }) => {
|
test('connects using ipv6', async ({ page, context }) => {
|
||||||
await utils.setAuthCookies(context, admin.accessToken, '[::1]');
|
let ipv6: string;
|
||||||
await page.goto('http://[::1]:2285/');
|
if (playwrightHost === '127.0.0.1') {
|
||||||
|
ipv6 = '::1';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const { address } = await lookup(playwrightHost, 6);
|
||||||
|
ipv6 = address;
|
||||||
|
} catch {
|
||||||
|
test.skip(true, 'No IPv6 address available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ipv6Url = `http://[${ipv6}]:2285/`;
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken, undefined, ipv6Url);
|
||||||
|
await page.goto(ipv6Url);
|
||||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||||
import { playwrightHost } from 'playwright.config';
|
import { playwrightHost } from 'playwright.config';
|
||||||
|
|
||||||
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
||||||
@@ -283,3 +283,13 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const waitForServiceWorker = async (page: Page) => {
|
||||||
|
await expect
|
||||||
|
.poll(() => page.context().serviceWorkers().length, {
|
||||||
|
message:
|
||||||
|
'Service worker not registered. Ensure the origin is a secure context (localhost or use --unsafely-treat-insecure-origin-as-secure flag).',
|
||||||
|
timeout: 10_000,
|
||||||
|
})
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
|
import { waitForServiceWorker } from 'src/ui/mock-network/base-network';
|
||||||
|
|
||||||
function getAssetIdFromUrl(url: URL): string | null {
|
function getAssetIdFromUrl(url: URL): string | null {
|
||||||
const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/);
|
const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/);
|
||||||
@@ -15,6 +16,7 @@ export const memoryViewerUtils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async waitForMemoryLoad(page: Page) {
|
async waitForMemoryLoad(page: Page) {
|
||||||
|
await waitForServiceWorker(page);
|
||||||
await expect(this.locator(page)).toBeVisible();
|
await expect(this.locator(page)).toBeVisible();
|
||||||
await expect(page.locator('#memory-viewer img').first()).toBeVisible();
|
await expect(page.locator('#memory-viewer img').first()).toBeVisible();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
|
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
|
||||||
|
import { waitForServiceWorker } from 'src/ui/mock-network/base-network';
|
||||||
|
|
||||||
export const sleep = (ms: number) => {
|
export const sleep = (ms: number) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -143,6 +144,7 @@ export const timelineUtils = {
|
|||||||
return page.locator('#asset-grid');
|
return page.locator('#asset-grid');
|
||||||
},
|
},
|
||||||
async waitForTimelineLoad(page: Page) {
|
async waitForTimelineLoad(page: Page) {
|
||||||
|
await waitForServiceWorker(page);
|
||||||
await expect(timelineUtils.locator(page)).toBeInViewport();
|
await expect(timelineUtils.locator(page)).toBeInViewport();
|
||||||
await expect.poll(() => thumbnailUtils.locator(page).count()).toBeGreaterThan(0);
|
await expect.poll(() => thumbnailUtils.locator(page).count()).toBeGreaterThan(0);
|
||||||
},
|
},
|
||||||
@@ -163,6 +165,7 @@ export const assetViewerUtils = {
|
|||||||
return page.locator('#immich-asset-viewer');
|
return page.locator('#immich-asset-viewer');
|
||||||
},
|
},
|
||||||
async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) {
|
async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) {
|
||||||
|
await waitForServiceWorker(page);
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`,
|
`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`,
|
||||||
|
|||||||
+9
-10
@@ -71,7 +71,7 @@ import { io, type Socket } from 'socket.io-client';
|
|||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { playwrightDbHost, playwrightHost, playwriteBaseUrl } from '../playwright.config';
|
import { playwrightDbHost, playwrightDbPort, playwrightHost, playwriteBaseUrl } from '../playwright.config';
|
||||||
|
|
||||||
export type { Emitter } from '@socket.io/component-emitter';
|
export type { Emitter } from '@socket.io/component-emitter';
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: nu
|
|||||||
type AdminSetupOptions = { onboarding?: boolean };
|
type AdminSetupOptions = { onboarding?: boolean };
|
||||||
type FileData = { bytes?: Buffer; filename: string };
|
type FileData = { bytes?: Buffer; filename: string };
|
||||||
|
|
||||||
const dbUrl = `postgres://postgres:postgres@${playwrightDbHost}:5435/immich`;
|
const dbUrl = `postgres://postgres:postgres@${playwrightDbHost}:${playwrightDbPort}/immich`;
|
||||||
export const baseUrl = playwriteBaseUrl;
|
export const baseUrl = playwriteBaseUrl;
|
||||||
export const shareUrl = `${baseUrl}/share`;
|
export const shareUrl = `${baseUrl}/share`;
|
||||||
export const app = `${baseUrl}/api`;
|
export const app = `${baseUrl}/api`;
|
||||||
@@ -522,13 +522,13 @@ export const utils = {
|
|||||||
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
||||||
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = playwrightHost) =>
|
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = playwrightHost, url?: string) => {
|
||||||
|
const origin = url ? { url } : { domain, path: '/' };
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
{
|
{
|
||||||
name: 'immich_access_token',
|
name: 'immich_access_token',
|
||||||
value: accessToken,
|
value: accessToken,
|
||||||
domain,
|
...origin,
|
||||||
path: '/',
|
|
||||||
expires: 2_058_028_213,
|
expires: 2_058_028_213,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
@@ -537,8 +537,7 @@ export const utils = {
|
|||||||
{
|
{
|
||||||
name: 'immich_auth_type',
|
name: 'immich_auth_type',
|
||||||
value: 'password',
|
value: 'password',
|
||||||
domain,
|
...origin,
|
||||||
path: '/',
|
|
||||||
expires: 2_058_028_213,
|
expires: 2_058_028_213,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
@@ -547,14 +546,14 @@ export const utils = {
|
|||||||
{
|
{
|
||||||
name: 'immich_is_authenticated',
|
name: 'immich_is_authenticated',
|
||||||
value: 'true',
|
value: 'true',
|
||||||
domain,
|
...origin,
|
||||||
path: '/',
|
|
||||||
expires: 2_058_028_213,
|
expires: 2_058_028_213,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
},
|
},
|
||||||
]),
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
setMaintenanceAuthCookie: async (context: BrowserContext, token: string, domain = '127.0.0.1') =>
|
setMaintenanceAuthCookie: async (context: BrowserContext, token: string, domain = '127.0.0.1') =>
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
|
|||||||
@@ -8409,7 +8409,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/MergeFaceClusterDto"
|
"$ref": "#/components/schemas/MergePersonDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -18878,10 +18878,10 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"MergeFaceClusterDto": {
|
"MergePersonDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ids": {
|
"ids": {
|
||||||
"description": "Face cluster IDs to merge",
|
"description": "Person IDs to merge",
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -23055,70 +23055,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SyncAssetFaceV3": {
|
|
||||||
"properties": {
|
|
||||||
"assetId": {
|
|
||||||
"description": "Asset ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"boundingBoxX1": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"boundingBoxX2": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"boundingBoxY1": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"boundingBoxY2": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"description": "Face deleted at",
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"faceClusterId": {
|
|
||||||
"description": "Face cluster ID",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"description": "Asset face ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"imageHeight": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"imageWidth": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"isVisible": {
|
|
||||||
"description": "Is the face visible in the asset",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"sourceType": {
|
|
||||||
"description": "Source type",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"assetId",
|
|
||||||
"boundingBoxX1",
|
|
||||||
"boundingBoxX2",
|
|
||||||
"boundingBoxY1",
|
|
||||||
"boundingBoxY2",
|
|
||||||
"deletedAt",
|
|
||||||
"faceClusterId",
|
|
||||||
"id",
|
|
||||||
"imageHeight",
|
|
||||||
"imageWidth",
|
|
||||||
"isVisible",
|
|
||||||
"sourceType"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SyncAssetMetadataDeleteV1": {
|
"SyncAssetMetadataDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetId": {
|
"assetId": {
|
||||||
@@ -23412,14 +23348,10 @@
|
|||||||
"StackV1",
|
"StackV1",
|
||||||
"StackDeleteV1",
|
"StackDeleteV1",
|
||||||
"PersonV1",
|
"PersonV1",
|
||||||
"PersonV2",
|
|
||||||
"PersonDeleteV1",
|
"PersonDeleteV1",
|
||||||
"AssetFaceV1",
|
"AssetFaceV1",
|
||||||
"AssetFaceV2",
|
"AssetFaceV2",
|
||||||
"AssetFaceV3",
|
|
||||||
"AssetFaceDeleteV1",
|
"AssetFaceDeleteV1",
|
||||||
"FaceClusterV1",
|
|
||||||
"FaceClusterDeleteV1",
|
|
||||||
"UserMetadataV1",
|
"UserMetadataV1",
|
||||||
"UserMetadataDeleteV1",
|
"UserMetadataDeleteV1",
|
||||||
"SyncAckV1",
|
"SyncAckV1",
|
||||||
@@ -23428,47 +23360,6 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"SyncFaceClusterDeleteV1": {
|
|
||||||
"properties": {
|
|
||||||
"faceClusterId": {
|
|
||||||
"description": "Face cluster ID",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"faceClusterId"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SyncFaceClusterV1": {
|
|
||||||
"properties": {
|
|
||||||
"createdAt": {
|
|
||||||
"description": "Created at",
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"description": "Face cluster ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ownerId": {
|
|
||||||
"description": "Owner ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"description": "Updated at",
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"createdAt",
|
|
||||||
"id",
|
|
||||||
"ownerId",
|
|
||||||
"updatedAt"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SyncMemoryAssetDeleteV1": {
|
"SyncMemoryAssetDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetId": {
|
"assetId": {
|
||||||
@@ -23711,75 +23602,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SyncPersonV2": {
|
|
||||||
"properties": {
|
|
||||||
"birthDate": {
|
|
||||||
"description": "Birth date",
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"description": "Color",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"description": "Created at",
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"faceAssetId": {
|
|
||||||
"description": "Face asset ID",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"faceClusterId": {
|
|
||||||
"description": "Face cluster ID",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"description": "Person ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isFavorite": {
|
|
||||||
"description": "Is favorite",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isHidden": {
|
|
||||||
"description": "Is hidden",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"description": "Person name",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ownerId": {
|
|
||||||
"description": "Owner ID",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"description": "Updated at",
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"birthDate",
|
|
||||||
"color",
|
|
||||||
"createdAt",
|
|
||||||
"faceAssetId",
|
|
||||||
"faceClusterId",
|
|
||||||
"id",
|
|
||||||
"isFavorite",
|
|
||||||
"isHidden",
|
|
||||||
"name",
|
|
||||||
"ownerId",
|
|
||||||
"updatedAt"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SyncRequestType": {
|
"SyncRequestType": {
|
||||||
"description": "Sync request types",
|
"description": "Sync request types",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -23802,11 +23624,8 @@
|
|||||||
"StacksV1",
|
"StacksV1",
|
||||||
"UsersV1",
|
"UsersV1",
|
||||||
"PeopleV1",
|
"PeopleV1",
|
||||||
"PeopleV2",
|
|
||||||
"AssetFacesV1",
|
"AssetFacesV1",
|
||||||
"AssetFacesV2",
|
"AssetFacesV2",
|
||||||
"AssetFacesV3",
|
|
||||||
"FaceClusterV1",
|
|
||||||
"UserMetadataV1"
|
"UserMetadataV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceUpdateDto,
|
AssetFaceUpdateDto,
|
||||||
MergeFaceClusterDto,
|
MergePersonDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -182,7 +182,7 @@ export class PersonController {
|
|||||||
mergePerson(
|
mergePerson(
|
||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
@Body() dto: MergeFaceClusterDto,
|
@Body() dto: MergePersonDto,
|
||||||
): Promise<BulkIdResponseDto[]> {
|
): Promise<BulkIdResponseDto[]> {
|
||||||
return this.service.mergePerson(auth, id, dto);
|
return this.service.mergePerson(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export type AssetFace = {
|
|||||||
boundingBoxY2: number;
|
boundingBoxY2: number;
|
||||||
imageHeight: number;
|
imageHeight: number;
|
||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
faceClusterId: string | null;
|
personId: string | null;
|
||||||
sourceType: SourceType;
|
sourceType: SourceType;
|
||||||
person?: ShallowDehydrateObject<Person> | null;
|
person?: ShallowDehydrateObject<Person> | null;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export class PeopleUpdateItem extends PersonUpdateDto {
|
|||||||
id!: string;
|
id!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MergeFaceClusterDto {
|
export class MergePersonDto {
|
||||||
@ValidateUUID({ each: true, description: 'Face cluster IDs to merge' })
|
@ValidateUUID({ each: true, description: 'Person IDs to merge' })
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -411,18 +411,6 @@ export class SyncPersonV1 {
|
|||||||
faceAssetId!: string | null;
|
faceAssetId!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
|
||||||
export class SyncPersonV2 extends SyncPersonV1 {
|
|
||||||
@ApiProperty({ description: 'Face cluster ID' })
|
|
||||||
faceClusterId!: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncPersonV2ToV1(personV2: SyncPersonV2): SyncPersonV1 {
|
|
||||||
const { faceClusterId: _, ...personV1 } = personV2;
|
|
||||||
|
|
||||||
return personV1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncPersonDeleteV1 {
|
export class SyncPersonDeleteV1 {
|
||||||
@ApiProperty({ description: 'Person ID' })
|
@ApiProperty({ description: 'Person ID' })
|
||||||
@@ -461,40 +449,6 @@ export class SyncAssetFaceV2 extends SyncAssetFaceV1 {
|
|||||||
isVisible!: boolean;
|
isVisible!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
|
||||||
export class SyncAssetFaceV3 {
|
|
||||||
@ApiProperty({ description: 'Asset face ID' })
|
|
||||||
id!: string;
|
|
||||||
@ApiProperty({ description: 'Asset ID' })
|
|
||||||
assetId!: string;
|
|
||||||
@ApiProperty({ description: 'Face cluster ID' })
|
|
||||||
faceClusterId!: string | null;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
imageWidth!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
imageHeight!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
boundingBoxX1!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
boundingBoxY1!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
boundingBoxX2!: number;
|
|
||||||
@ApiProperty({ type: 'integer' })
|
|
||||||
boundingBoxY2!: number;
|
|
||||||
@ApiProperty({ description: 'Source type' })
|
|
||||||
sourceType!: string;
|
|
||||||
@ApiProperty({ description: 'Face deleted at' })
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
@ApiProperty({ description: 'Is the face visible in the asset' })
|
|
||||||
isVisible!: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncAssetFaceV3ToV2(faceV3: SyncAssetFaceV3, personId: string | null): SyncAssetFaceV2 {
|
|
||||||
const { faceClusterId: _, ...face } = faceV3;
|
|
||||||
|
|
||||||
return { ...face, personId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 {
|
export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 {
|
||||||
const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2;
|
const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2;
|
||||||
|
|
||||||
@@ -507,24 +461,6 @@ export class SyncAssetFaceDeleteV1 {
|
|||||||
assetFaceId!: string;
|
assetFaceId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
|
||||||
export class SyncFaceClusterV1 {
|
|
||||||
@ApiProperty({ description: 'Face cluster ID' })
|
|
||||||
id!: string;
|
|
||||||
@ApiProperty({ description: 'Created at' })
|
|
||||||
createdAt!: Date;
|
|
||||||
@ApiProperty({ description: 'Updated at' })
|
|
||||||
updatedAt!: Date;
|
|
||||||
@ApiProperty({ description: 'Owner ID' })
|
|
||||||
ownerId!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExtraModel()
|
|
||||||
export class SyncFaceClusterDeleteV1 {
|
|
||||||
@ApiProperty({ description: 'Face cluster ID' })
|
|
||||||
faceClusterId!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncUserMetadataV1 {
|
export class SyncUserMetadataV1 {
|
||||||
@ApiProperty({ description: 'User ID' })
|
@ApiProperty({ description: 'User ID' })
|
||||||
@@ -594,14 +530,10 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1;
|
[SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1;
|
||||||
[SyncEntityType.PartnerStackV1]: SyncStackV1;
|
[SyncEntityType.PartnerStackV1]: SyncStackV1;
|
||||||
[SyncEntityType.PersonV1]: SyncPersonV1;
|
[SyncEntityType.PersonV1]: SyncPersonV1;
|
||||||
[SyncEntityType.PersonV2]: SyncPersonV2;
|
|
||||||
[SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1;
|
[SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1;
|
||||||
[SyncEntityType.AssetFaceV1]: SyncAssetFaceV1;
|
[SyncEntityType.AssetFaceV1]: SyncAssetFaceV1;
|
||||||
[SyncEntityType.AssetFaceV2]: SyncAssetFaceV2;
|
[SyncEntityType.AssetFaceV2]: SyncAssetFaceV2;
|
||||||
[SyncEntityType.AssetFaceV3]: SyncAssetFaceV3;
|
|
||||||
[SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1;
|
[SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1;
|
||||||
[SyncEntityType.FaceClusterV1]: SyncFaceClusterV1;
|
|
||||||
[SyncEntityType.FaceClusterDeleteV1]: SyncFaceClusterDeleteV1;
|
|
||||||
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
||||||
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||||
|
|||||||
@@ -735,11 +735,8 @@ export enum SyncRequestType {
|
|||||||
StacksV1 = 'StacksV1',
|
StacksV1 = 'StacksV1',
|
||||||
UsersV1 = 'UsersV1',
|
UsersV1 = 'UsersV1',
|
||||||
PeopleV1 = 'PeopleV1',
|
PeopleV1 = 'PeopleV1',
|
||||||
PeopleV2 = 'PeopleV2',
|
|
||||||
AssetFacesV1 = 'AssetFacesV1',
|
AssetFacesV1 = 'AssetFacesV1',
|
||||||
AssetFacesV2 = 'AssetFacesV2',
|
AssetFacesV2 = 'AssetFacesV2',
|
||||||
AssetFacesV3 = 'AssetFacesV3',
|
|
||||||
FaceClusterV1 = 'FaceClusterV1',
|
|
||||||
UserMetadataV1 = 'UserMetadataV1',
|
UserMetadataV1 = 'UserMetadataV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,17 +794,12 @@ export enum SyncEntityType {
|
|||||||
StackDeleteV1 = 'StackDeleteV1',
|
StackDeleteV1 = 'StackDeleteV1',
|
||||||
|
|
||||||
PersonV1 = 'PersonV1',
|
PersonV1 = 'PersonV1',
|
||||||
PersonV2 = 'PersonV2',
|
|
||||||
PersonDeleteV1 = 'PersonDeleteV1',
|
PersonDeleteV1 = 'PersonDeleteV1',
|
||||||
|
|
||||||
AssetFaceV1 = 'AssetFaceV1',
|
AssetFaceV1 = 'AssetFaceV1',
|
||||||
AssetFaceV2 = 'AssetFaceV2',
|
AssetFaceV2 = 'AssetFaceV2',
|
||||||
AssetFaceV3 = 'AssetFaceV3',
|
|
||||||
AssetFaceDeleteV1 = 'AssetFaceDeleteV1',
|
AssetFaceDeleteV1 = 'AssetFaceDeleteV1',
|
||||||
|
|
||||||
FaceClusterV1 = 'FaceClusterV1',
|
|
||||||
FaceClusterDeleteV1 = 'FaceClusterDeleteV1',
|
|
||||||
|
|
||||||
UserMetadataV1 = 'UserMetadataV1',
|
UserMetadataV1 = 'UserMetadataV1',
|
||||||
UserMetadataDeleteV1 = 'UserMetadataDeleteV1',
|
UserMetadataDeleteV1 = 'UserMetadataDeleteV1',
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export interface AssetFaceId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFacesData {
|
export interface UpdateFacesData {
|
||||||
oldFaceClusterId?: string;
|
oldPersonId?: string;
|
||||||
faceIds?: string[];
|
faceIds?: string[];
|
||||||
newFaceClusterId: string;
|
newPersonId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonStatistics {
|
export interface PersonStatistics {
|
||||||
@@ -54,7 +54,7 @@ export interface GetAllPeopleOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllFacesOptions {
|
export interface GetAllFacesOptions {
|
||||||
faceClusterId?: string | null;
|
personId?: string | null;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
sourceType?: SourceType;
|
sourceType?: SourceType;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
|||||||
|
|
||||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||||
return jsonObjectFrom(
|
return jsonObjectFrom(
|
||||||
eb.selectFrom('person').selectAll('person').whereRef('person.faceClusterId', '=', 'asset_face.faceClusterId'),
|
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'),
|
||||||
).as('person');
|
).as('person');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,11 +80,11 @@ export class PersonRepository {
|
|||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||||
async reassignFaces({ oldFaceClusterId, faceIds, newFaceClusterId }: UpdateFacesData): Promise<number> {
|
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: newFaceClusterId })
|
.set({ personId: newPersonId })
|
||||||
.$if(!!oldFaceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', oldFaceClusterId!))
|
.$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!))
|
||||||
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export class PersonRepository {
|
|||||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: null })
|
.set({ personId: null })
|
||||||
.where('asset_face.sourceType', '=', sourceType)
|
.where('asset_face.sourceType', '=', sourceType)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -117,8 +117,8 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.$if(options.faceClusterId === null, (qb) => qb.where('asset_face.faceClusterId', 'is', null))
|
.$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null))
|
||||||
.$if(!!options.faceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', options.faceClusterId!))
|
.$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!))
|
||||||
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
||||||
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
@@ -153,7 +153,7 @@ export class PersonRepository {
|
|||||||
const items = await this.db
|
const items = await this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.innerJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
.innerJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||||
.innerJoin('asset', (join) =>
|
.innerJoin('asset', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset_face.assetId', '=', 'asset.id')
|
.onRef('asset_face.assetId', '=', 'asset.id')
|
||||||
@@ -209,7 +209,7 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.leftJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
.leftJoin('asset_face', 'asset_face.personId', 'person.id')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
||||||
@@ -248,7 +248,7 @@ export class PersonRepository {
|
|||||||
getFaceForFacialRecognitionJob(id: string) {
|
getFaceForFacialRecognitionJob(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select(['asset_face.id', 'asset_face.faceClusterId', 'asset_face.sourceType'])
|
.select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType'])
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
jsonObjectFrom(
|
jsonObjectFrom(
|
||||||
eb
|
eb
|
||||||
@@ -297,10 +297,10 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
async reassignFace(assetFaceId: string, newFaceClusterId: string): Promise<number> {
|
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ faceClusterId: newFaceClusterId })
|
.set({ personId: newPersonId })
|
||||||
.where('asset_face.id', '=', assetFaceId)
|
.where('asset_face.id', '=', assetFaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -346,13 +346,13 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getStatistics(faceClusterId: string): Promise<PersonStatistics> {
|
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.leftJoin('asset', (join) =>
|
.leftJoin('asset', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset.id', '=', 'asset_face.assetId')
|
.onRef('asset.id', '=', 'asset_face.assetId')
|
||||||
.on('asset_face.faceClusterId', '=', faceClusterId)
|
.on('asset_face.personId', '=', personId)
|
||||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||||
.on('asset.deletedAt', 'is', null),
|
.on('asset.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
@@ -375,7 +375,7 @@ export class PersonRepository {
|
|||||||
eb.exists((eb) =>
|
eb.exists((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.whereRef('asset_face.faceClusterId', '=', 'person.faceClusterId')
|
.whereRef('asset_face.personId', '=', 'person.id')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', '=', true)
|
.where('asset_face.isVisible', '=', true)
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
@@ -395,16 +395,7 @@ export class PersonRepository {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(person: Insertable<PersonTable>) {
|
create(person: Insertable<PersonTable>) {
|
||||||
if (!person.faceClusterId) {
|
|
||||||
const { id } = await this.db
|
|
||||||
.insertInto('face_cluster')
|
|
||||||
.values({ ownerId: person.ownerId })
|
|
||||||
.returning('id')
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
person.faceClusterId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,19 +486,18 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson)
|
||||||
.innerJoin('person', (join) => join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId'))
|
|
||||||
.where('person.id', 'in', personIds)
|
|
||||||
.where('asset_face.assetId', 'in', assetIds)
|
.where('asset_face.assetId', 'in', assetIds)
|
||||||
|
.where('asset_face.personId', 'in', personIds)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getRandomFace(faceClusterId: string) {
|
getRandomFace(personId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.where('asset_face.faceClusterId', '=', faceClusterId)
|
.where('asset_face.personId', '=', personId)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -594,9 +584,7 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('asset_face.id')
|
.select('asset_face.id')
|
||||||
.where('asset_face.assetId', '=', assetId)
|
.where('asset_face.assetId', '=', assetId)
|
||||||
.innerJoin('person', (join) =>
|
.where('asset_face.personId', '=', personId)
|
||||||
join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId').on('person.id', '=', personId),
|
|
||||||
)
|
|
||||||
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,15 +338,15 @@ export class SearchRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'asset_face.faceClusterId',
|
'asset_face.personId',
|
||||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||||
])
|
])
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
||||||
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
.leftJoin('person', 'person.id', 'asset_face.personId')
|
||||||
.where('asset.ownerId', '=', anyUuid(userIds))
|
.where('asset.ownerId', '=', anyUuid(userIds))
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
.$if(!!hasPerson, (qb) => qb.where('asset_face.faceClusterId', 'is not', null))
|
.$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null))
|
||||||
.$if(!!minBirthDate, (qb) =>
|
.$if(!!minBirthDate, (qb) =>
|
||||||
qb.where((eb) =>
|
qb.where((eb) =>
|
||||||
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export class SyncRepository {
|
|||||||
assetFace: AssetFaceSync;
|
assetFace: AssetFaceSync;
|
||||||
assetMetadata: AssetMetadataSync;
|
assetMetadata: AssetMetadataSync;
|
||||||
authUser: AuthUserSync;
|
authUser: AuthUserSync;
|
||||||
faceCluster: FaceClusterSync;
|
|
||||||
memory: MemorySync;
|
memory: MemorySync;
|
||||||
memoryToAsset: MemoryToAssetSync;
|
memoryToAsset: MemoryToAssetSync;
|
||||||
partner: PartnerSync;
|
partner: PartnerSync;
|
||||||
@@ -81,7 +80,6 @@ export class SyncRepository {
|
|||||||
this.assetFace = new AssetFaceSync(this.db);
|
this.assetFace = new AssetFaceSync(this.db);
|
||||||
this.assetMetadata = new AssetMetadataSync(this.db);
|
this.assetMetadata = new AssetMetadataSync(this.db);
|
||||||
this.authUser = new AuthUserSync(this.db);
|
this.authUser = new AuthUserSync(this.db);
|
||||||
this.faceCluster = new FaceClusterSync(this.db);
|
|
||||||
this.memory = new MemorySync(this.db);
|
this.memory = new MemorySync(this.db);
|
||||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||||
this.partner = new PartnerSync(this.db);
|
this.partner = new PartnerSync(this.db);
|
||||||
@@ -449,7 +447,6 @@ class PersonSync extends BaseSync {
|
|||||||
'color',
|
'color',
|
||||||
'updateId',
|
'updateId',
|
||||||
'faceAssetId',
|
'faceAssetId',
|
||||||
'faceClusterId',
|
|
||||||
])
|
])
|
||||||
.where('ownerId', '=', options.userId)
|
.where('ownerId', '=', options.userId)
|
||||||
.stream();
|
.stream();
|
||||||
@@ -476,7 +473,7 @@ class AssetFaceSync extends BaseSync {
|
|||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'assetId',
|
'assetId',
|
||||||
'faceClusterId',
|
'personId',
|
||||||
'imageWidth',
|
'imageWidth',
|
||||||
'imageHeight',
|
'imageHeight',
|
||||||
'boundingBoxX1',
|
'boundingBoxX1',
|
||||||
@@ -488,8 +485,6 @@ class AssetFaceSync extends BaseSync {
|
|||||||
'asset_face.deletedAt',
|
'asset_face.deletedAt',
|
||||||
'asset_face.updateId',
|
'asset_face.updateId',
|
||||||
])
|
])
|
||||||
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
|
||||||
.select('person.id as personId')
|
|
||||||
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
.where('asset.ownerId', '=', options.userId)
|
.where('asset.ownerId', '=', options.userId)
|
||||||
.where('asset_face.isVisible', '=', true)
|
.where('asset_face.isVisible', '=', true)
|
||||||
@@ -497,35 +492,6 @@ class AssetFaceSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FaceClusterSync extends BaseSync {
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
|
||||||
getDeletes(options: SyncQueryOptions) {
|
|
||||||
return this.auditQuery('face_cluster_audit', options)
|
|
||||||
.select(['face_cluster_audit.id', 'face_cluster_audit.faceClusterId'])
|
|
||||||
.leftJoin('face_cluster', 'face_cluster.id', 'face_cluster_audit.id')
|
|
||||||
.where('face_cluster.ownerId', '=', options.userId)
|
|
||||||
.stream();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupAuditTable(daysAgo: number) {
|
|
||||||
return this.auditCleanup('face_cluster_audit', daysAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
|
||||||
getUpserts(options: SyncQueryOptions) {
|
|
||||||
return this.upsertQuery('face_cluster', options)
|
|
||||||
.select([
|
|
||||||
'face_cluster.id',
|
|
||||||
'face_cluster.createdAt',
|
|
||||||
'face_cluster.updatedAt',
|
|
||||||
'face_cluster.ownerId',
|
|
||||||
'face_cluster.updateId',
|
|
||||||
])
|
|
||||||
.where('face_cluster.ownerId', '=', options.userId)
|
|
||||||
.stream();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssetExifSync extends BaseSync {
|
class AssetExifSync extends BaseSync {
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
|
|||||||
@@ -299,16 +299,3 @@ export const asset_edit_audit = registerFunction({
|
|||||||
RETURN NULL;
|
RETURN NULL;
|
||||||
END`,
|
END`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const face_cluster_delete_audit = registerFunction({
|
|
||||||
name: 'face_cluster_delete_audit',
|
|
||||||
returnType: 'TRIGGER',
|
|
||||||
language: 'PLPGSQL',
|
|
||||||
body: `
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO face_cluster_audit ("faceClusterId", "ownerId")
|
|
||||||
SELECT "id", "ownerId"
|
|
||||||
FROM OLD;
|
|
||||||
RETURN NULL;
|
|
||||||
END`,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table';
|
|||||||
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||||
import { FaceClusterAuditTable } from 'src/schema/tables/face-cluster-audit.table';
|
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|
||||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
@@ -201,8 +199,6 @@ export interface DB {
|
|||||||
|
|
||||||
audit: AuditTable;
|
audit: AuditTable;
|
||||||
|
|
||||||
face_cluster: FaceClusterTable;
|
|
||||||
face_cluster_audit: FaceClusterAuditTable;
|
|
||||||
face_search: FaceSearchTable;
|
face_search: FaceSearchTable;
|
||||||
|
|
||||||
geodata_places: GeodataPlacesTable;
|
geodata_places: GeodataPlacesTable;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { SourceType } from 'src/enum';
|
|||||||
import { asset_face_source_type } from 'src/schema/enums';
|
import { asset_face_source_type } from 'src/schema/enums';
|
||||||
import { asset_face_audit } from 'src/schema/functions';
|
import { asset_face_audit } from 'src/schema/functions';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
|
|
||||||
@Table({ name: 'asset_face' })
|
@Table({ name: 'asset_face' })
|
||||||
@UpdatedAtTrigger('asset_face_updatedAt')
|
@UpdatedAtTrigger('asset_face_updatedAt')
|
||||||
@@ -26,13 +26,13 @@ import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|||||||
when: 'pg_trigger_depth() = 0',
|
when: 'pg_trigger_depth() = 0',
|
||||||
})
|
})
|
||||||
// schemaFromDatabase does not preserve column order
|
// schemaFromDatabase does not preserve column order
|
||||||
@Index({ name: 'asset_face_assetId_faceClusterId_idx', columns: ['assetId', 'faceClusterId'] })
|
@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] })
|
||||||
@Index({
|
@Index({
|
||||||
name: 'asset_face_faceClusterId_assetId_notDeleted_isVisible_idx',
|
name: 'asset_face_personId_assetId_notDeleted_isVisible_idx',
|
||||||
columns: ['faceClusterId', 'assetId'],
|
columns: ['personId', 'assetId'],
|
||||||
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
||||||
})
|
})
|
||||||
@Index({ columns: ['faceClusterId', 'assetId'] })
|
@Index({ columns: ['personId', 'assetId'] })
|
||||||
export class AssetFaceTable {
|
export class AssetFaceTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
@@ -45,14 +45,14 @@ export class AssetFaceTable {
|
|||||||
})
|
})
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => FaceClusterTable, {
|
@ForeignKeyColumn(() => PersonTable, {
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
// [faceClusterId, assetId] makes this redundant
|
// [personId, assetId] makes this redundant
|
||||||
index: false,
|
index: false,
|
||||||
})
|
})
|
||||||
faceClusterId!: string | null;
|
personId!: string | null;
|
||||||
|
|
||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
imageWidth!: Generated<number>;
|
imageWidth!: Generated<number>;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
|
|
||||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
|
||||||
|
|
||||||
@Table('face_cluster_audit')
|
|
||||||
export class FaceClusterAuditTable {
|
|
||||||
@PrimaryGeneratedUuidV7Column()
|
|
||||||
id!: Generated<string>;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', index: true })
|
|
||||||
faceClusterId!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid', index: true })
|
|
||||||
ownerId!: string;
|
|
||||||
|
|
||||||
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
|
||||||
deletedAt!: Generated<Timestamp>;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {
|
|
||||||
AfterDeleteTrigger,
|
|
||||||
CreateDateColumn,
|
|
||||||
ForeignKeyColumn,
|
|
||||||
Generated,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Table,
|
|
||||||
Timestamp,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from '@immich/sql-tools';
|
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
|
||||||
import { face_cluster_delete_audit } from 'src/schema/functions';
|
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
|
||||||
|
|
||||||
@Table('face_cluster')
|
|
||||||
@UpdatedAtTrigger('face_cluster_updatedAt')
|
|
||||||
@AfterDeleteTrigger({
|
|
||||||
scope: 'statement',
|
|
||||||
function: face_cluster_delete_audit,
|
|
||||||
referencingOldTableAs: 'old',
|
|
||||||
when: 'pg_trigger_depth() = 0',
|
|
||||||
})
|
|
||||||
export class FaceClusterTable {
|
|
||||||
@PrimaryGeneratedColumn('uuid')
|
|
||||||
id!: Generated<string>;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
|
||||||
updateId!: Generated<string>;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
|
||||||
ownerId!: string;
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { person_delete_audit } from 'src/schema/functions';
|
import { person_delete_audit } from 'src/schema/functions';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
@Table('person')
|
@Table('person')
|
||||||
@@ -61,7 +60,4 @@ export class PersonTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => FaceClusterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true, index: true })
|
|
||||||
faceClusterId!: string | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,7 +876,7 @@ export class MetadataService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
this.logger.debug(`Creating missing people: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
this.logger.debugFn(() => `Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
||||||
const newPersonIds = await this.personRepository.createAll(missing);
|
const newPersonIds = await this.personRepository.createAll(missing);
|
||||||
const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const);
|
const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const);
|
||||||
await this.jobRepository.queueAll(jobs);
|
await this.jobRepository.queueAll(jobs);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
FaceDto,
|
FaceDto,
|
||||||
mapFaces,
|
mapFaces,
|
||||||
mapPerson,
|
mapPerson,
|
||||||
MergeFaceClusterDto,
|
MergePersonDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -438,7 +438,7 @@ export class PersonService extends BaseService {
|
|||||||
|
|
||||||
const lastRun = new Date().toISOString();
|
const lastRun = new Date().toISOString();
|
||||||
const facePagination = this.personRepository.getAllFaces(
|
const facePagination = this.personRepository.getAllFaces(
|
||||||
force ? undefined : { faceClusterId: null, sourceType: SourceType.MachineLearning },
|
force ? undefined : { personId: null, sourceType: SourceType.MachineLearning },
|
||||||
);
|
);
|
||||||
|
|
||||||
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
||||||
@@ -481,8 +481,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (face.faceClusterId) {
|
if (face.personId) {
|
||||||
this.logger.debug(`Face ${id} already belongs to a face cluster`);
|
this.logger.debug(`Face ${id} already has a person assigned`);
|
||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,8 +511,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
let faceClusterId = matches.find((match) => match.faceClusterId)?.faceClusterId;
|
let personId = matches.find((match) => match.personId)?.personId;
|
||||||
if (!faceClusterId) {
|
if (!personId) {
|
||||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
const matchWithPerson = await this.searchRepository.searchFaces({
|
||||||
userIds: [face.asset.ownerId],
|
userIds: [face.asset.ownerId],
|
||||||
embedding: face.faceSearch.embedding,
|
embedding: face.faceSearch.embedding,
|
||||||
@@ -523,20 +523,20 @@ export class PersonService extends BaseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchWithPerson.length > 0) {
|
if (matchWithPerson.length > 0) {
|
||||||
faceClusterId = matchWithPerson[0].faceClusterId;
|
personId = matchWithPerson[0].personId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCore && !faceClusterId) {
|
if (isCore && !personId) {
|
||||||
this.logger.log(`Creating new person for face ${id}`);
|
this.logger.log(`Creating new person for face ${id}`);
|
||||||
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
||||||
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
||||||
faceClusterId = newPerson.faceClusterId;
|
personId = newPerson.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (faceClusterId) {
|
if (personId) {
|
||||||
this.logger.debug(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
this.logger.debug(`Assigning face ${id} to person ${personId}`);
|
||||||
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
@@ -554,7 +554,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
async mergePerson(auth: AuthDto, id: string, dto: MergeFaceClusterDto): Promise<BulkIdResponseDto[]> {
|
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
||||||
const mergeIds = dto.ids;
|
const mergeIds = dto.ids;
|
||||||
if (mergeIds.includes(id)) {
|
if (mergeIds.includes(id)) {
|
||||||
throw new BadRequestException('Cannot merge a person into themselves');
|
throw new BadRequestException('Cannot merge a person into themselves');
|
||||||
@@ -600,7 +600,7 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergeName = mergePerson.name || mergePerson.id;
|
const mergeName = mergePerson.name || mergePerson.id;
|
||||||
const mergeData: UpdateFacesData = { oldFaceClusterId: mergeId, newFaceClusterId: id };
|
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||||
|
|
||||||
await this.personRepository.reassignFaces(mergeData);
|
await this.personRepository.reassignFaces(mergeData);
|
||||||
@@ -678,14 +678,8 @@ export class PersonService extends BaseService {
|
|||||||
dto.imageHeight = originalDimensions.height;
|
dto.imageHeight = originalDimensions.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const person = await this.personRepository.getById(dto.personId);
|
|
||||||
|
|
||||||
if (!person?.faceClusterId) {
|
|
||||||
throw new Error('Person must already have some recognized faces and belong to a face cluster');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.personRepository.createAssetFace({
|
await this.personRepository.createAssetFace({
|
||||||
faceClusterId: person.faceClusterId,
|
personId: dto.personId,
|
||||||
assetId: dto.assetId,
|
assetId: dto.assetId,
|
||||||
imageHeight: dto.imageHeight,
|
imageHeight: dto.imageHeight,
|
||||||
imageWidth: dto.imageWidth,
|
imageWidth: dto.imageWidth,
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import {
|
|||||||
SyncAckDeleteDto,
|
SyncAckDeleteDto,
|
||||||
SyncAckSetDto,
|
SyncAckSetDto,
|
||||||
syncAssetFaceV2ToV1,
|
syncAssetFaceV2ToV1,
|
||||||
syncAssetFaceV3ToV2,
|
|
||||||
SyncAssetV1,
|
SyncAssetV1,
|
||||||
SyncItem,
|
SyncItem,
|
||||||
syncPersonV2ToV1,
|
|
||||||
SyncStreamDto,
|
SyncStreamDto,
|
||||||
} from 'src/dtos/sync.dto';
|
} from 'src/dtos/sync.dto';
|
||||||
import {
|
import {
|
||||||
@@ -194,11 +192,8 @@ export class SyncService extends BaseService {
|
|||||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PeopleV2]: () => this.syncPeopleV2(options, response, checkpointMap),
|
|
||||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetFacesV3]: async () => this.syncAssetFacesV3(options, response, checkpointMap),
|
|
||||||
[SyncRequestType.FaceClusterV1]: async () => this.syncFaceClusterV1(options, response, checkpointMap),
|
|
||||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -801,20 +796,6 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.PersonV1;
|
const upsertType = SyncEntityType.PersonV1;
|
||||||
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, ...data } of upserts) {
|
|
||||||
send(response, { type: upsertType, ids: [updateId], data: syncPersonV2ToV1(data) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async syncPeopleV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
|
||||||
const deleteType = SyncEntityType.PersonDeleteV1;
|
|
||||||
const deletes = this.syncRepository.person.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
|
||||||
for await (const { id, ...data } of deletes) {
|
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
|
||||||
}
|
|
||||||
|
|
||||||
const upsertType = SyncEntityType.PersonV2;
|
|
||||||
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
@@ -829,8 +810,8 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV1;
|
const upsertType = SyncEntityType.AssetFaceV1;
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, personId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
const v1 = syncAssetFaceV2ToV1(syncAssetFaceV3ToV2(data, personId));
|
const v1 = syncAssetFaceV2ToV1(data);
|
||||||
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,34 +825,6 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV2;
|
const upsertType = SyncEntityType.AssetFaceV2;
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, personId, ...data } of upserts) {
|
|
||||||
send(response, { type: upsertType, ids: [updateId], data: syncAssetFaceV3ToV2(data, personId) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async syncAssetFacesV3(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
|
||||||
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
|
||||||
const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
|
||||||
for await (const { id, ...data } of deletes) {
|
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
|
||||||
}
|
|
||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV3;
|
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
|
||||||
for await (const { updateId, personId: _, ...data } of upserts) {
|
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async syncFaceClusterV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
|
||||||
const deleteType = SyncEntityType.FaceClusterDeleteV1;
|
|
||||||
const deletes = this.syncRepository.faceCluster.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
|
||||||
for await (const { id, ...data } of deletes) {
|
|
||||||
send(response, { type: deleteType, ids: [id], data });
|
|
||||||
}
|
|
||||||
|
|
||||||
const upsertType = SyncEntityType.FaceClusterV1;
|
|
||||||
const upserts = this.syncRepository.faceCluster.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,12 +144,7 @@ export function withFacesAndPeople(
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
(eb) =>
|
(eb) =>
|
||||||
eb
|
eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'),
|
||||||
.selectFrom('face_cluster')
|
|
||||||
.where('face_cluster.id', '=', 'asset_face.faceClusterId')
|
|
||||||
.innerJoin('person', 'person.faceClusterId', 'face_cluster.id')
|
|
||||||
.selectAll('person')
|
|
||||||
.as('person'),
|
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
@@ -166,12 +161,11 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'asset', O>, personIds:
|
|||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('assetId')
|
.select('assetId')
|
||||||
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
.where('personId', '=', anyUuid(personIds!))
|
||||||
.where('person.id', '=', anyUuid(personIds!))
|
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.where('isVisible', 'is', true)
|
.where('isVisible', 'is', true)
|
||||||
.groupBy('assetId')
|
.groupBy('assetId')
|
||||||
.having((eb) => eb.fn.count('person.id').distinct(), '=', personIds.length)
|
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
.as('has_people'),
|
.as('has_people'),
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user