name: Test on: workflow_dispatch: pull_request: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: pre-job: runs-on: ubuntu-latest permissions: contents: read outputs: should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_mobile: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }} should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - id: found_paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | i18n: - 'i18n/**' web: - 'web/**' - 'i18n/**' - 'open-api/typescript-sdk/**' server: - 'server/**' cli: - 'cli/**' - 'open-api/typescript-sdk/**' e2e: - 'e2e/**' mobile: - 'mobile/**' machine-learning: - 'machine-learning/**' workflow: - '.github/workflows/test.yml' .github: - '.github/**' - name: Check if we should force jobs to run id: should_force run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" server-unit-tests: name: Test & Lint Server needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./server steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci - name: Run linter run: npm run lint if: ${{ !cancelled() }} - name: Run formatter run: npm run format if: ${{ !cancelled() }} - name: Run tsc run: npm run check if: ${{ !cancelled() }} - name: Run small tests & coverage run: npm test if: ${{ !cancelled() }} cli-unit-tests: name: Unit Test CLI needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./cli steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './cli/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk - name: Install deps run: npm ci - name: Run linter run: npm run lint if: ${{ !cancelled() }} - name: Run formatter run: npm run format if: ${{ !cancelled() }} - name: Run tsc run: npm run check if: ${{ !cancelled() }} - name: Run unit tests & coverage run: npm run test if: ${{ !cancelled() }} cli-unit-tests-win: name: Unit Test CLI (Windows) needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: windows-latest permissions: contents: read defaults: run: working-directory: ./cli steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './cli/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk - name: Install deps run: npm ci # Skip linter & formatter in Windows test. - name: Run tsc run: npm run check if: ${{ !cancelled() }} - name: Run unit tests & coverage run: npm run test if: ${{ !cancelled() }} web-lint: name: Lint Web needs: pre-job if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: mich permissions: contents: read defaults: run: working-directory: ./web steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk - name: Run npm install run: npm ci - name: Run linter run: npm run lint:p if: ${{ !cancelled() }} - name: Run formatter run: npm run format if: ${{ !cancelled() }} - name: Run svelte checks run: npm run check:svelte if: ${{ !cancelled() }} web-unit-tests: name: Test Web needs: pre-job if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./web steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk - name: Run npm install run: npm ci - name: Run tsc run: npm run check:typescript if: ${{ !cancelled() }} - name: Run unit tests & coverage run: npm run test if: ${{ !cancelled() }} i18n-tests: name: Test i18n needs: pre-job if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }} runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './web/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Install dependencies run: npm --prefix=web ci - name: Format run: npm --prefix=web run format:i18n - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files with: files: | i18n/** - name: Verify files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' env: CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: i18n files not up to date!" echo "Changed files: ${CHANGED_FILES}" exit 1 e2e-tests-lint: name: End-to-End Lint needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./e2e steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk if: ${{ !cancelled() }} - name: Install dependencies run: npm ci if: ${{ !cancelled() }} - name: Run linter run: npm run lint if: ${{ !cancelled() }} - name: Run formatter run: npm run format if: ${{ !cancelled() }} - name: Run tsc run: npm run check if: ${{ !cancelled() }} server-medium-tests: name: Medium Tests (Server) needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./server steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci - name: Run medium tests run: npm run test:medium if: ${{ !cancelled() }} e2e-tests-server-cli: name: End-to-End Tests (Server & CLI) needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }} runs-on: ${{ matrix.runner }} permissions: contents: read defaults: run: working-directory: ./e2e strategy: matrix: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false submodules: 'recursive' - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk if: ${{ !cancelled() }} - name: Run setup cli run: npm ci && npm run build working-directory: ./cli if: ${{ !cancelled() }} - name: Install dependencies run: npm ci if: ${{ !cancelled() }} - name: Docker build run: docker compose build if: ${{ !cancelled() }} - name: Run e2e tests (api & cli) run: npm run test if: ${{ !cancelled() }} e2e-tests-web: name: End-to-End Tests (Web) needs: pre-job if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }} runs-on: ${{ matrix.runner }} permissions: contents: read defaults: run: working-directory: ./e2e strategy: matrix: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false submodules: 'recursive' - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './e2e/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk if: ${{ !cancelled() }} - name: Install dependencies run: npm ci if: ${{ !cancelled() }} - name: Install Playwright Browsers run: npx playwright install chromium --only-shell if: ${{ !cancelled() }} - name: Docker build run: docker compose build if: ${{ !cancelled() }} - name: Run e2e tests (web) run: npx playwright test if: ${{ !cancelled() }} success-check-e2e: name: End-to-End Tests Success needs: [e2e-tests-server-cli, e2e-tests-web] permissions: {} runs-on: ubuntu-latest if: always() steps: - uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 with: needs: ${{ toJSON(needs) }} mobile-unit-tests: name: Unit Test Mobile needs: pre-job if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Flutter SDK uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml - name: Generate translation file run: make translation working-directory: ./mobile - name: Run tests working-directory: ./mobile run: flutter test -j 1 ml-unit-tests: name: Unit Test ML needs: pre-job if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./machine-learning steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # with: # python-version: 3.11 # cache: 'uv' - name: Install dependencies run: | uv sync --extra cpu - name: Lint with ruff run: | uv run ruff check --output-format=github immich_ml - name: Check black formatting run: | uv run black --check immich_ml - name: Run mypy type checking run: | uv run mypy --strict immich_ml/ - name: Run tests and coverage run: | uv run pytest --cov=immich_ml --cov-report term-missing github-files-formatting: name: .github Files Formatting needs: pre-job if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }} runs-on: ubuntu-latest permissions: contents: read defaults: run: working-directory: ./.github steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './.github/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Run npm install run: npm ci - name: Run formatter run: npm run format if: ${{ !cancelled() }} shellcheck: name: ShellCheck runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Run ShellCheck uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 with: ignore_paths: >- **/open-api/** **/openapi** **/node_modules/** generated-api-up-to-date: name: OpenAPI Clients runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Install server dependencies run: npm --prefix=server ci - name: Build the app run: npm --prefix=server run build - name: Run API generation run: make open-api - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files with: files: | mobile/openapi open-api/typescript-sdk open-api/immich-openapi-specs.json - name: Verify files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' env: CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: Generated files not up to date!" echo "Changed files: ${CHANGED_FILES}" exit 1 sql-schema-up-to-date: name: SQL Schema Checks runs-on: ubuntu-latest permissions: contents: read services: postgres: image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: immich options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 defaults: run: working-directory: ./server steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: './server/.nvmrc' cache: 'npm' cache-dependency-path: '**/package-lock.json' - name: Install server dependencies run: npm ci - name: Build the app run: npm run build - name: Run existing migrations run: npm run migrations:run - name: Test npm run schema:reset command works run: npm run schema:reset - name: Generate new migrations continue-on-error: true run: npm run migrations:generate src/TestMigration - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files with: files: | server/src - name: Verify migration files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' env: CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }} run: | echo "ERROR: Generated migration files not up to date!" echo "Changed files: ${CHANGED_FILES}" cat ./src/*-TestMigration.ts exit 1 - name: Run SQL generation run: npm run sync:sql env: DB_URL: postgres://postgres:postgres@localhost:5432/immich - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-sql-files with: files: | server/src/queries - name: Verify SQL files have not changed if: steps.verify-changed-sql-files.outputs.files_changed == 'true' env: CHANGED_FILES: ${{ steps.verify-changed-sql-files.outputs.changed_files }} run: | echo "ERROR: Generated SQL files not up to date!" echo "Changed files: ${CHANGED_FILES}" git diff exit 1 # mobile-integration-tests: # name: Run mobile end-to-end integration tests # runs-on: macos-latest # steps: # - uses: actions/checkout@v4 # - uses: actions/setup-java@v3 # with: # distribution: 'zulu' # java-version: '12.x' # cache: 'gradle' # - name: Cache android SDK # uses: actions/cache@v3 # id: android-sdk # with: # key: android-sdk # path: | # /usr/local/lib/android/ # ~/.android # - name: Cache Gradle # uses: actions/cache@v3 # with: # path: | # ./mobile/build/ # ./mobile/android/.gradle/ # key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }} # - name: Setup Android SDK # if: steps.android-sdk.outputs.cache-hit != 'true' # uses: android-actions/setup-android@v2 # - name: AVD cache # uses: actions/cache@v3 # id: avd-cache # with: # path: | # ~/.android/avd/* # ~/.android/adb* # key: avd-29 # - name: create AVD and generate snapshot for caching # if: steps.avd-cache.outputs.cache-hit != 'true' # uses: reactivecircus/android-emulator-runner@v2.27.0 # with: # working-directory: ./mobile # cores: 2 # api-level: 29 # arch: x86_64 # profile: pixel # target: default # force-avd-creation: false # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none # disable-animations: false # script: echo "Generated AVD snapshot for caching." # - name: Setup Flutter SDK # uses: subosito/flutter-action@v2 # with: # channel: 'stable' # flutter-version: '3.7.3' # cache: true # - name: Run integration tests # uses: Wandalen/wretry.action@master # with: # action: reactivecircus/android-emulator-runner@v2.27.0 # with: | # working-directory: ./mobile # cores: 2 # api-level: 29 # arch: x86_64 # profile: pixel # target: default # force-avd-creation: false # emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none # disable-animations: true # script: | # flutter pub get # flutter test integration_test # attempt_limit: 3