immich-app/immich

23 workflows · maturity 83% · 11 patterns · GitHub ↗

Security 55.91/100

Practices

✓ Matrix✓ Permissions✓ Security scan○ AI review✓ Cache✓ Concurrency✓ Reusable workflows

Detected patterns

Security dimensions

permissions
21.7
security scan
12.5
supply chain
6.7
secret handling
15
harden runner
0

Tools: github/codeql-action/analyze, github/codeql-action/autobuild, github/codeql-action/init

Workflows (23)

auto-close perms .github/workflows/auto-close.yml
Triggers
pull_request_target
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
parse_template, close_template, close_llm, reopen
Commands
  • OK=true while IFS= read -r header; do printf '%s\n' "$BODY" | grep -qF "$header" || OK=false done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ") echo "uses_template=$OK" >> "$GITHUB_OUTPUT"
  • gh api graphql \ -f prId="$NODE_ID" \ -f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \ -f query=' mutation CommentAndClosePR($prId: ID!, $body: String!) { addComment(input: { subjectId: $prId, body: $body }) { __typename } closePullRequest(input: { pullRequestId: $prId }) { __typename } }'
  • gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "auto-closed:template"
  • gh api graphql \ -f prId="$NODE_ID" \ -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ -f query=' mutation CommentAndClosePR($prId: ID!, $body: String!) { addComment(input: { subjectId: $prId, body: $body }) { __typename } closePullRequest(input: { pullRequestId: $prId }) { __typename } }'
  • gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label "auto-closed:template" || true
  • REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \ --jq '[.labels[].name | select(startswith("auto-closed:"))] | length') echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT"
  • gh api graphql \ -f prId="$NODE_ID" \ -f query=' mutation ReopenPR($prId: ID!) { reopenPullRequest(input: { pullRequestId: $prId }) { __typename } }'
View raw YAML
name: Auto-close PRs

on:
  pull_request_target: # zizmor: ignore[dangerous-triggers]
    types: [opened, edited, labeled]

permissions: {}

jobs:
  parse_template:
    runs-on: ubuntu-latest
    if: ${{ github.event.action != 'labeled' && github.event.pull_request.head.repo.fork == true }}
    permissions:
      contents: read
    outputs:
      uses_template: ${{ steps.check.outputs.uses_template }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          sparse-checkout: .github/pull_request_template.md
          sparse-checkout-cone-mode: false
          persist-credentials: false

      - name: Check required sections
        id: check
        env:
          BODY: ${{ github.event.pull_request.body }}
        run: |
          OK=true
          while IFS= read -r header; do
            printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
          done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ")
          echo "uses_template=$OK" >> "$GITHUB_OUTPUT"

  close_template:
    runs-on: ubuntu-latest
    needs: parse_template
    if: >-
      ${{
        needs.parse_template.outputs.uses_template == 'false'
        && github.event.pull_request.state != 'closed'
        && !contains(github.event.pull_request.labels.*.name, 'auto-closed:template')
      }}
    permissions:
      pull-requests: write
    steps:
      - name: Comment and close
        env:
          GH_TOKEN: ${{ github.token }}
          NODE_ID: ${{ github.event.pull_request.node_id }}
        run: |
          gh api graphql \
            -f prId="$NODE_ID" \
            -f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \
            -f query='
              mutation CommentAndClosePR($prId: ID!, $body: String!) {
                addComment(input: {
                  subjectId: $prId,
                  body: $body
                }) {
                  __typename
                }
                closePullRequest(input: {
                  pullRequestId: $prId
                }) {
                  __typename
                }
              }'

      - name: Add label
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "auto-closed:template"

  close_llm:
    runs-on: ubuntu-latest
    if: ${{ github.event.action == 'labeled' && github.event.label.name == 'auto-closed:llm' }}
    permissions:
      pull-requests: write
    steps:
      - name: Comment and close
        env:
          GH_TOKEN: ${{ github.token }}
          NODE_ID: ${{ github.event.pull_request.node_id }}
        run: |
          gh api graphql \
            -f prId="$NODE_ID" \
            -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \
            -f query='
              mutation CommentAndClosePR($prId: ID!, $body: String!) {
                addComment(input: {
                  subjectId: $prId,
                  body: $body
                }) {
                  __typename
                }
                closePullRequest(input: {
                  pullRequestId: $prId
                }) {
                  __typename
                }
              }'

  reopen:
    runs-on: ubuntu-latest
    needs: parse_template
    if: >-
      ${{
        needs.parse_template.outputs.uses_template == 'true'
        && github.event.pull_request.state == 'closed'
        && contains(github.event.pull_request.labels.*.name, 'auto-closed:template')
      }}
    permissions:
      pull-requests: write
    steps:
      - name: Remove template label
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label "auto-closed:template" || true

      - name: Check for remaining auto-closed labels
        id: check_labels
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \
            --jq '[.labels[].name | select(startswith("auto-closed:"))] | length')
          echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT"

      - name: Reopen PR
        if: ${{ steps.check_labels.outputs.remaining == '0' }}
        env:
          GH_TOKEN: ${{ github.token }}
          NODE_ID: ${{ github.event.pull_request.node_id }}
        run: |
          gh api graphql \
            -f prId="$NODE_ID" \
            -f query='
              mutation ReopenPR($prId: ID!) {
                reopenPullRequest(input: {
                  pullRequestId: $prId
                }) {
                  __typename
                }
              }'
build-mobile perms .github/workflows/build-mobile.yml
Triggers
workflow_call, pull_request, push
Runs on
ubuntu-latest, mich, macos-15
Jobs
pre-job, build-sign-android, build-sign-ios
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, immich-app/devtools/actions/create-workflow-token, subosito/flutter-action, android-actions/setup-android, subosito/flutter-action, ruby/setup-ruby
Commands
  • printf "%s" $KEY_JKS | base64 -d > android/key.jks
  • flutter pub get
  • dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
  • make pigeon
  • if [[ $IS_MAIN == 'true' ]]; then flutter build apk --release flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 else flutter build apk --debug --split-per-abi --target-platform android-arm64 fi
  • sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
  • flutter pub get
  • dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
View raw YAML
name: Build Mobile

on:
  workflow_call:
    inputs:
      ref:
        required: false
        type: string
      environment:
        description: 'Target environment'
        required: true
        default: 'development'
        type: string
    secrets:
      KEY_JKS:
        required: true
      ALIAS:
        required: true
      ANDROID_KEY_PASSWORD:
        required: true
      ANDROID_STORE_PASSWORD:
        required: true
      APP_STORE_CONNECT_API_KEY_ID:
        required: true
      APP_STORE_CONNECT_API_KEY_ISSUER_ID:
        required: true
      APP_STORE_CONNECT_API_KEY:
        required: true
      IOS_CERTIFICATE_P12:
        required: true
      IOS_CERTIFICATE_PASSWORD:
        required: true
      FASTLANE_TEAM_ID:
        required: true
  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: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          filters: |
            mobile:
              - 'mobile/**'
          force-filters: |
            - '.github/workflows/build-mobile.yml'
          force-events: 'workflow_call,workflow_dispatch'

  build-sign-android:
    name: Build and sign Android
    needs: pre-job
    permissions:
      contents: read
    # Skip when PR from a fork
    if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
    runs-on: mich

    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ inputs.ref || github.sha }}
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      - name: Create the Keystore
        env:
          KEY_JKS: ${{ secrets.KEY_JKS }}
        working-directory: ./mobile
        run: printf "%s" $KEY_JKS | base64 -d > android/key.jks

      - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Restore Gradle Cache
        id: cache-gradle-restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
            ~/.android/sdk
            mobile/android/.gradle
            mobile/.dart_tool
          key: build-mobile-gradle-${{ runner.os }}-main

      - name: Setup Flutter SDK
        uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
        with:
          channel: 'stable'
          flutter-version-file: ./mobile/pubspec.yaml
          cache: true

      - name: Setup Android SDK
        uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
        with:
          packages: ''

      - name: Get Packages
        working-directory: ./mobile
        run: flutter pub get

      - name: Generate translation file
        run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
        working-directory: ./mobile

      - name: Generate platform APIs
        run: make pigeon
        working-directory: ./mobile

      - name: Build Android App Bundle
        working-directory: ./mobile
        env:
          ALIAS: ${{ secrets.ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
          ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
          IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
        run: |
          if [[ $IS_MAIN == 'true' ]]; then
            flutter build apk --release
            flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
          else
            flutter build apk --debug --split-per-abi --target-platform android-arm64
          fi

      - name: Publish Android Artifact
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: release-apk-signed
          path: mobile/build/app/outputs/flutter-apk/*.apk

      - name: Save Gradle Cache
        id: cache-gradle-save
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        if: github.ref == 'refs/heads/main'
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
            ~/.android/sdk
            mobile/android/.gradle
            mobile/.dart_tool
          key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}

  build-sign-ios:
    name: Build and sign iOS
    needs: pre-job
    permissions:
      contents: read
    # Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
    if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
    runs-on: macos-15

    steps:
      - name: Select Xcode 26
        run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer

      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ inputs.ref || github.sha }}
          persist-credentials: false

      - name: Setup Flutter SDK
        uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
        with:
          channel: 'stable'
          flutter-version-file: ./mobile/pubspec.yaml
          cache: true

      - name: Install Flutter dependencies
        working-directory: ./mobile
        run: flutter pub get

      - name: Generate translation files
        run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
        working-directory: ./mobile

      - name: Generate platform APIs
        run: make pigeon
        working-directory: ./mobile

      - name: Setup Ruby
        uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
        with:
          ruby-version: '3.3'
          bundler-cache: true
          working-directory: ./mobile/ios

      - name: Install CocoaPods dependencies
        working-directory: ./mobile/ios
        run: |
          pod install

      - name: Create API Key
        env:
          API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
          API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
        working-directory: ./mobile/ios
        run: |
          mkdir -p ~/.appstoreconnect/private_keys
          echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8

      - name: Import Certificate
        env:
          IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
        working-directory: ./mobile/ios
        run: |
          # Decode certificate
          echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12

      - name: Create keychain and import certificate
        env:
          KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
          CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
        working-directory: ./mobile/ios
        run: |
          # Create keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security set-keychain-settings -t 3600 -u build.keychain

          # Import certificate
          security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
          security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain

          # Verify certificate was imported
          security find-identity -v -p codesigning build.keychain

      - name: Build and deploy to TestFlight
        env:
          FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }}
          IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
          KEYCHAIN_NAME: build.keychain
          KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
          ENVIRONMENT: ${{ inputs.environment || 'development' }}
          BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
          GITHUB_REF: ${{ github.ref }}
          FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
          FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
        working-directory: ./mobile/ios
        run: |
          # Only upload to TestFlight on main branch
          if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
            if [[ "$ENVIRONMENT" == "development" ]]; then
              bundle exec fastlane gha_testflight_dev
            else
              bundle exec fastlane gha_release_prod
            fi
          else
            # Build only, no TestFlight upload for non-main branches
            bundle exec fastlane gha_build_only
          fi

      - name: Clean up keychain
        if: always()
        run: |
          security delete-keychain build.keychain || true

      - name: Upload IPA artifact
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: ios-release-ipa
          path: mobile/ios/Runner.ipa
cache-cleanup perms .github/workflows/cache-cleanup.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
cleanup
Actions
immich-app/devtools/actions/create-workflow-token
Commands
  • gh extension install actions/gh-actions-cache REPO=${{ github.repository }} echo "Fetching list of cache keys" cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 ) ## Setting this to not fail the workflow while deleting cache keys. set +e echo "Deleting caches..." for cacheKey in $cacheKeysForPR do gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm done echo "Done"
View raw YAML
name: Cache Cleanup
on:
  pull_request:
    types:
      - closed

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  cleanup:
    name: Cleanup
    runs-on: ubuntu-latest
    permissions:
      contents: read
      actions: write
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check out code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      - name: Cleanup
        env:
          GH_TOKEN: ${{ steps.token.outputs.token }}
          REF: ${{ github.ref }}
        run: |
          gh extension install actions/gh-actions-cache

          REPO=${{ github.repository }}

          echo "Fetching list of cache keys"
          cacheKeysForPR=$(gh actions-cache list -R $REPO -B ${REF} -L 100 | cut -f 1 )

          ## Setting this to not fail the workflow while deleting cache keys.
          set +e
          echo "Deleting caches..."
          for cacheKey in $cacheKeysForPR
          do
              gh actions-cache delete $cacheKey -R "$REPO" -B "${REF}" --confirm
          done
          echo "Done"
check-openapi perms .github/workflows/check-openapi.yml
Triggers
workflow_dispatch, pull_request
Runs on
ubuntu-latest
Jobs
check-openapi
Actions
oasdiff/oasdiff-action/breaking
View raw YAML
name: Check OpenAPI
on:
  workflow_dispatch:
  pull_request:
    paths:
      - 'open-api/**'
      - '.github/workflows/check-openapi.yml'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  check-openapi:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Check for breaking API changes
        uses: oasdiff/oasdiff-action/breaking@2a37bc82462349c03a533b8b608bebbaf57b3e60 # v0.0.33
        with:
          base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
          revision: open-api/immich-openapi-specs.json
          fail-on: ERR
cli perms .github/workflows/cli.yml
Triggers
push, pull_request, release
Runs on
ubuntu-latest, ubuntu-latest
Jobs
publish, docker
Actions
immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, docker/setup-qemu-action, docker/setup-buildx-action, docker/login-action, docker/metadata-action, docker/build-push-action
Commands
  • pnpm install && pnpm run build
  • pnpm install --frozen-lockfile
  • pnpm build
  • pnpm publish --provenance --no-git-checks
  • version=$(jq -r '.version' cli/package.json) echo "version=$version" >> "$GITHUB_OUTPUT"
View raw YAML
name: CLI Build
on:
  push:
    branches: [main]
    paths:
      - 'cli/**'
      - '.github/workflows/cli.yml'
  pull_request:
    paths:
      - 'cli/**'
      - '.github/workflows/cli.yml'
  release:
    types: [published]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  publish:
    name: CLI Publish
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
    defaults:
      run:
        working-directory: ./cli
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './cli/.nvmrc'
          registry-url: 'https://registry.npmjs.org'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'

      - name: Setup typescript-sdk
        run: pnpm install && pnpm run build
        working-directory: ./open-api/typescript-sdk

      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - run: pnpm publish --provenance --no-git-checks
        if: ${{ github.event_name == 'release' }}

  docker:
    name: Docker
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    needs: publish

    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

      - name: Login to GitHub Container Registry
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        if: ${{ !github.event.pull_request.head.repo.fork }}
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Get package version
        id: package-version
        run: |
          version=$(jq -r '.version' cli/package.json)
          echo "version=$version" >> "$GITHUB_OUTPUT"

      - name: Generate docker image tags
        id: metadata
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          flavor: |
            latest=false
          images: |
            name=ghcr.io/${{ github.repository_owner }}/immich-cli
          tags: |
            type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
            type=raw,value=latest,enable=${{ github.event_name == 'release' }}

      - name: Build and push image
        uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
        with:
          file: cli/Dockerfile
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name == 'release' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          tags: ${{ steps.metadata.outputs.tags }}
          labels: ${{ steps.metadata.outputs.labels }}
close-duplicates perms .github/workflows/close-duplicates.yml
Triggers
issues, discussion
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
should_run, get_body, get_checkbox_json, close_and_comment
Commands
  • echo "run=${{ github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request' }}" >> $GITHUB_OUTPUT
  • BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0) echo "body=$BODY" >> $GITHUB_OUTPUT
  • CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false') echo "checked=$CHECKED" >> $GITHUB_OUTPUT
  • gh api graphql \ -f issueId="$NODE_ID" \ -f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \ -f query=' mutation CommentAndCloseIssue($issueId: ID!, $body: String!) { addComment(input: { subjectId: $issueId, body: $body }) { __typename } closeIssue(input: { issueId: $issueId, stateReason: DUPLICATE }) { __typename } }'
  • gh api graphql \ -f discussionId="$NODE_ID" \ -f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \ -f query=' mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { __typename } closeDiscussion(input: { discussionId: $discussionId, reason: DUPLICATE }) { __typename } }'
View raw YAML
on:
  issues:
    types: [opened]
  discussion:
    types: [created]

name: Close likely duplicates
permissions: {}

jobs:
  should_run:
    runs-on: ubuntu-latest
    outputs:
      should_run: ${{ steps.should_run.outputs.run }}
    steps:
      - id: should_run
        run: echo "run=${{ github.event_name == 'issues' || github.event.discussion.category.name == 'Feature Request' }}" >> $GITHUB_OUTPUT

  get_body:
    runs-on: ubuntu-latest
    needs: should_run
    if: ${{ needs.should_run.outputs.should_run == 'true' }}
    env:
      EVENT: ${{ toJSON(github.event) }}
    outputs:
      body: ${{ steps.get_body.outputs.body }}
    steps:
      - id: get_body
        run: |
          BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0)
          echo "body=$BODY" >> $GITHUB_OUTPUT

  get_checkbox_json:
    runs-on: ubuntu-latest
    needs: [get_body, should_run]
    if: ${{ needs.should_run.outputs.should_run == 'true' }}
    container:
      image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
    outputs:
      checked: ${{ steps.get_checkbox.outputs.checked }}
    steps:
      - id: get_checkbox
        env:
          BODY: ${{ needs.get_body.outputs.body }}
        run: |
          CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false')
          echo "checked=$CHECKED" >> $GITHUB_OUTPUT

  close_and_comment:
    runs-on: ubuntu-latest
    needs: [get_checkbox_json, should_run]
    if: ${{ needs.should_run.outputs.should_run == 'true' && needs.get_checkbox_json.outputs.checked != 'true' }}
    permissions:
      issues: write
      discussions: write
    steps:
      - name: Close issue
        if: ${{ github.event_name == 'issues' }}
        env:
          GH_TOKEN: ${{ github.token }}
          NODE_ID: ${{ github.event.issue.node_id }}
        run: |
          gh api graphql \
            -f issueId="$NODE_ID" \
            -f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
            -f query='
              mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
                addComment(input: {
                  subjectId: $issueId, 
                  body: $body
                }) {
                  __typename
                }
              
                closeIssue(input: {
                  issueId: $issueId,
                  stateReason: DUPLICATE
                }) {
                  __typename
                }
              }'

      - name: Close discussion
        if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
        env:
          GH_TOKEN: ${{ github.token }}
          NODE_ID: ${{ github.event.discussion.node_id }}
        run: |
          gh api graphql \
            -f discussionId="$NODE_ID" \
            -f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one. If you're sure this is not a duplicate, please leave a comment and we will reopen the thread if necessary." \
            -f query='
              mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
                addDiscussionComment(input: {
                  discussionId: $discussionId,
                  body: $body
                }) {
                  __typename
                }
              
                closeDiscussion(input: {
                  discussionId: $discussionId,
                  reason: DUPLICATE
                }) {
                  __typename
                }
              }'
codeql-analysis matrix perms security .github/workflows/codeql-analysis.yml
Triggers
push, pull_request, schedule
Runs on
ubuntu-latest
Jobs
analyze
Matrix
language→ javascript, python
Actions
immich-app/devtools/actions/create-workflow-token, github/codeql-action/init, github/codeql-action/autobuild, github/codeql-action/analyze
View raw YAML
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'

on:
  push:
    branches: ['main']
  pull_request:
    # The branches below must be a subset of the branches above
    branches: ['main']
  schedule:
    - cron: '20 13 * * 1'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: ['javascript', 'python']
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
        with:
          languages: ${{ matrix.language }}
          # If you wish to specify custom queries, you can do so here or in a config file.
          # By default, queries listed here will override any specified in a config file.
          # Prefix the list here with "+" to use these queries and those in the config file.

          # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
          # queries: security-extended,security-and-quality

      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
      # If this step fails, then you should remove it and run the build manually (see below)
      - name: Autobuild
        uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0

      # ℹ️ 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

      #   If the Autobuild fails above, remove it and uncomment the following three lines.
      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

      # - run: |
      #   echo "Run, Build Application using script"
      #   ./location_of_script_within_repo/buildscript.sh

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
        with:
          category: '/language:${{matrix.language}}'
docker matrix perms .github/workflows/docker.yml
Triggers
workflow_dispatch, push, pull_request, release
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
pre-job, retag_ml, retag_server, machine-learning, server, success-check-server, success-check-ml
Matrix
include, include.device, include.platforms, include.runner-mapping, include.suffixes, suffix→ , -armnn, -cuda, -openvino, -rknn, -rocm, armnn, cpu, cuda, linux/amd64, linux/arm64, openvino, rknn, rocm, {"linux/amd64": "pokedex-large"}
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, docker/login-action, docker/login-action, immich-app/devtools/actions/success-check, immich-app/devtools/actions/success-check
Commands
  • docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
  • docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
View raw YAML
name: Docker

on:
  workflow_dispatch:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  pre-job:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          filters: |
            server:
              - 'server/**'
              - 'openapi/**'
              - 'web/**'
              - 'i18n/**'
            machine-learning:
              - 'machine-learning/**'
          force-filters: |
            - '.github/workflows/docker.yml'
            - '.github/workflows/multi-runner-build.yml'
            - '.github/actions/image-build'
          force-events: 'workflow_dispatch,release'

  retag_ml:
    name: Re-Tag ML
    needs: pre-job
    permissions:
      contents: read
      packages: write
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == false && !github.event.pull_request.head.repo.fork }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
    steps:
      - name: Login to GitHub Container Registry
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Re-tag image
        env:
          REGISTRY_NAME: 'ghcr.io'
          REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning
          TAG_OLD: main${{ matrix.suffix }}
          TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
          TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
        run: |
          docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
          docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"

  retag_server:
    name: Re-Tag Server
    needs: pre-job
    permissions:
      contents: read
      packages: write
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        suffix: ['']
    steps:
      - name: Login to GitHub Container Registry
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Re-tag image
        env:
          REGISTRY_NAME: 'ghcr.io'
          REPOSITORY: ${{ github.repository_owner }}/immich-server
          TAG_OLD: main${{ matrix.suffix }}
          TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
          TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
        run: |
          docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
          docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"

  machine-learning:
    name: Build and Push ML
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - device: cpu
          - device: cuda
            suffixes: '-cuda'
            platforms: linux/amd64
          - device: openvino
            suffixes: '-openvino'
            platforms: linux/amd64
          - device: armnn
            suffixes: '-armnn'
            platforms: linux/arm64
          - device: rknn
            suffixes: '-rknn'
            platforms: linux/arm64
          - device: rocm
            suffixes: '-rocm'
            platforms: linux/amd64
            runner-mapping: '{"linux/amd64": "pokedex-large"}'
    uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
    permissions:
      contents: read
      actions: read
      packages: write
    secrets:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
    with:
      image: immich-machine-learning
      context: machine-learning
      dockerfile: machine-learning/Dockerfile
      platforms: ${{ matrix.platforms }}
      runner-mapping: ${{ matrix.runner-mapping }}
      suffixes: ${{ matrix.suffixes }}
      dockerhub-push: ${{ github.event_name == 'release' }}
      build-args: |
        DEVICE=${{ matrix.device }}

  server:
    name: Build and Push Server
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
    uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
    permissions:
      contents: read
      actions: read
      packages: write
    secrets:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
    with:
      image: immich-server
      context: .
      dockerfile: server/Dockerfile
      dockerhub-push: ${{ github.event_name == 'release' }}
      build-args: |
        DEVICE=cpu

  success-check-server:
    name: Docker Build & Push Server Success
    needs: [server, retag_server]
    permissions: {}
    runs-on: ubuntu-latest
    if: always()
    steps:
      - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
        with:
          needs: ${{ toJSON(needs) }}

  success-check-ml:
    name: Docker Build & Push ML Success
    needs: [machine-learning, retag_ml]
    permissions: {}
    runs-on: ubuntu-latest
    if: always()
    steps:
      - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
        with:
          needs: ${{ toJSON(needs) }}
docs-build perms .github/workflows/docs-build.yml
Triggers
push, pull_request, release
Runs on
ubuntu-latest, ubuntu-latest
Jobs
pre-job, build
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup
Commands
  • pnpm install
  • pnpm format
  • pnpm build
View raw YAML
name: Docs build
on:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

jobs:
  pre-job:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          filters: |
            docs:
              - 'docs/**'
            open-api:
              - 'open-api/immich-openapi-specs.json'
          force-filters: |
            - '.github/workflows/docs-build.yml'
          force-events: 'release'
          force-branches: 'main'

  build:
    name: Docs Build
    needs: pre-job
    permissions:
      contents: read
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).docs == true }}
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./docs

    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './docs/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'

      - name: Run install
        run: pnpm install

      - name: Check formatting
        run: pnpm format

      - name: Run build
        run: pnpm build

      - name: Upload build output
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: docs-build-output
          path: docs/build/
          include-hidden-files: true
          retention-days: 1
docs-deploy .github/workflows/docs-deploy.yml
Triggers
workflow_run
Runs on
ubuntu-latest, ubuntu-latest
Jobs
checks, deploy
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/use-mise, actions-cool/maintain-one-comment
Commands
  • echo 'The triggering workflow did not succeed' && exit 1
  • unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"
  • mise run //deployment:tf apply
  • mise run //deployment:tf output -- -json | jq -r ' "projectName=\(.pages_project_name.value)", "subdomain=\(.immich_app_branch_subdomain.value)" ' >> $GITHUB_OUTPUT
  • mise run //docs:deploy
  • mise run //deployment:tf apply
View raw YAML
name: Docs deploy
on:
  workflow_run: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
    workflows: ['Docs build']
    types:
      - completed

env:
  TG_NON_INTERACTIVE: 'true'

jobs:
  checks:
    name: Docs Deploy Checks
    runs-on: ubuntu-latest
    permissions:
      actions: read
      pull-requests: read
    outputs:
      parameters: ${{ steps.parameters.outputs.result }}
      artifact: ${{ steps.get-artifact.outputs.result }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - if: ${{ github.event.workflow_run.conclusion != 'success' }}
        run: echo 'The triggering workflow did not succeed' && exit 1
      - name: Get artifact
        id: get-artifact
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        with:
          github-token: ${{ steps.token.outputs.token }}
          script: |
            let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
               owner: context.repo.owner,
               repo: context.repo.repo,
               run_id: context.payload.workflow_run.id,
            });
            let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
              return artifact.name == "docs-build-output"
            })[0];
            if (!matchArtifact) {
              console.log("No artifact found with the name docs-build-output, build job was skipped")
              return { found: false };
            }
            return { found: true, id: matchArtifact.id };
      - name: Determine deploy parameters
        id: parameters
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        env:
          HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
        with:
          github-token: ${{ steps.token.outputs.token }}
          script: |
            const eventType = context.payload.workflow_run.event;
            const isFork = context.payload.workflow_run.repository.fork;

            let parameters;

            console.log({eventType, isFork});

            if (eventType == "push") {
              const branch = context.payload.workflow_run.head_branch;
              console.log({branch});
              const shouldDeploy = !isFork && branch == "main";
              parameters = {
                event: "branch",
                name: "main",
                shouldDeploy
              };
            } else if (eventType == "pull_request") {
              let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
              if(!pull_number) {
                const {HEAD_SHA} = process.env;
                const response = await github.rest.search.issuesAndPullRequests({q: `repo:${{ github.repository }} is:pr sha:${HEAD_SHA}`,per_page: 1,})
                const items = response.data.items
                if (items.length < 1) {
                  throw new Error("No pull request found for the commit")
                }
                const pullRequestNumber = items[0].number
                console.info("Pull request number is", pullRequestNumber)
                pull_number = pullRequestNumber
              }
              const {data: pr} = await github.rest.pulls.get({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number
              });

              console.log({pull_number});

              parameters = {
                event: "pr",
                name: `pr-${pull_number}`,
                pr_number: pull_number,
                shouldDeploy: true
              };
            } else if (eventType == "release") {
              parameters = {
                event: "release",
                name: context.payload.workflow_run.head_branch,
                shouldDeploy: !isFork
              };
            }

            console.log(parameters);
            return parameters;

  deploy:
    name: Docs Deploy
    runs-on: ubuntu-latest
    needs: checks
    permissions:
      contents: read
      actions: read
      pull-requests: write
    if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}

      - name: Setup Mise
        uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3

      - name: Load parameters
        id: parameters
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        env:
          PARAM_JSON: ${{ needs.checks.outputs.parameters }}
        with:
          github-token: ${{ steps.token.outputs.token }}
          script: |
            const parameters = JSON.parse(process.env.PARAM_JSON);
            core.setOutput("event", parameters.event);
            core.setOutput("name", parameters.name);
            core.setOutput("shouldDeploy", parameters.shouldDeploy);

      - name: Download artifact
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        env:
          ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
        with:
          github-token: ${{ steps.token.outputs.token }}
          script: |
            let artifact = JSON.parse(process.env.ARTIFACT_JSON);
            let download = await github.rest.actions.downloadArtifact({
               owner: context.repo.owner,
               repo: context.repo.repo,
               artifact_id: artifact.id,
               archive_format: 'zip',
            });
            let fs = require('fs');
            fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data));

      - name: Unzip artifact
        run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"

      - name: Deploy Docs Subdomain
        env:
          TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
          TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
        working-directory: 'deployment/modules/cloudflare/docs'
        run: 'mise run //deployment:tf apply'

      - name: Deploy Docs Subdomain Output
        id: docs-output
        env:
          TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
          TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
        working-directory: 'deployment/modules/cloudflare/docs'
        run: |
          mise run //deployment:tf output -- -json | jq -r '
            "projectName=\(.pages_project_name.value)",
            "subdomain=\(.immich_app_branch_subdomain.value)"
          ' >> $GITHUB_OUTPUT

      - name: Publish to Cloudflare Pages
        working-directory: docs
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          PROJECT_NAME: ${{ steps.docs-output.outputs.projectName }}
          BRANCH_NAME: ${{ steps.parameters.outputs.name }}
        run: mise run //docs:deploy

      - name: Deploy Docs Release Domain
        if: ${{ steps.parameters.outputs.event == 'release' }}
        env:
          TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
        working-directory: 'deployment/modules/cloudflare/docs-release'
        run: 'mise run //deployment:tf apply'

      - name: Comment
        uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
        if: ${{ steps.parameters.outputs.event == 'pr' }}
        with:
          token: ${{ steps.token.outputs.token }}
          number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
          body: |
            📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }})
          emojis: 'rocket'
          body-include: '<!-- Docs PR URL -->'
docs-destroy perms .github/workflows/docs-destroy.yml
Triggers
pull_request_target
Runs on
ubuntu-latest
Jobs
deploy
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/use-mise, actions-cool/maintain-one-comment
Commands
  • mise run //deployment:tf destroy -- -refresh=false
View raw YAML
name: Docs destroy
on:
  pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
    types: [closed]

permissions: {}

env:
  TG_NON_INTERACTIVE: 'true'

jobs:
  deploy:
    name: Docs Destroy
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}

      - name: Setup Mise
        uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3

      - name: Destroy Docs Subdomain
        env:
          TF_VAR_prefix_name: 'pr-${{ github.event.number }}'
          TF_VAR_prefix_event_type: 'pr'
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
        working-directory: 'deployment/modules/cloudflare/docs'
        run: 'mise run //deployment:tf destroy -- -refresh=false'

      - name: Comment
        uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
        with:
          token: ${{ steps.token.outputs.token }}
          number: ${{ github.event.number }}
          delete: true
          body-include: '<!-- Docs PR URL -->'
fix-format perms .github/workflows/fix-format.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
fix-formatting
Actions
actions/create-github-app-token, pnpm/action-setup, EndBug/add-and-commit
Commands
  • pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
View raw YAML
name: Fix formatting

on:
  pull_request:
    types: [labeled]

permissions: {}

jobs:
  fix-formatting:
    runs-on: ubuntu-latest
    if: ${{ github.event.label.name == 'fix:formatting' }}
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Generate a token
        id: generate-token
        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: 'Checkout'
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          token: ${{ steps.generate-token.outputs.token }}
          persist-credentials: true

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'

      - name: Fix formatting
        run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix

      - name: Commit and push
        uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
        with:
          default_author: github_actions
          message: 'chore: fix formatting'

      - name: Remove label
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        if: always()
        with:
          github-token: ${{ steps.generate-token.outputs.token }}
          script: |
            github.rest.issues.removeLabel({
              issue_number: context.payload.pull_request.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              name: 'fix:formatting'
            })
merge-translations perms .github/workflows/merge-translations.yml
Triggers
workflow_dispatch, workflow_call
Runs on
ubuntu-latest
Jobs
merge
Actions
actions/create-github-app-token
Commands
  • set -euo pipefail PR=$(gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable) echo "$PR" PR_NUMBER=$(echo "$PR" | jq ' if length == 1 then .[0].number else error("Expected exactly 1 entry, got \(length)") end ' 2>&1) || exit 1 echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT echo "Selected PR $PR_NUMBER" if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then echo "PR is not mergeable" exit 1 fi
  • curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=true
  • curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=commit curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=push
  • set -euo pipefail REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \ | jq '.id') echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash
  • # So we clean up no matter what set +e for i in {1..100}; do if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state | jq -e '.state == "MERGED"'; then echo "PR merged" exit 0 else echo "PR not merged yet, waiting..." sleep 6 fi done echo "PR did not merge in time" gh api -X PUT "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" --field message='Merge attempt timed out' --field event='DISMISS' gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --disable-auto exit 1
  • curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=false
  • echo "Workflow completed successfully (or was skipped)"
View raw YAML
name: Merge translations

on:
  workflow_dispatch:
  workflow_call:
    secrets:
      PUSH_O_MATIC_APP_ID:
        required: true
      PUSH_O_MATIC_APP_KEY:
        required: true
      WEBLATE_TOKEN:
        required: true
    inputs:
      skip:
        description: 'Skip translations'
        required: false
        type: boolean

permissions: {}

env:
  WEBLATE_HOST: 'https://hosted.weblate.org'
  WEBLATE_COMPONENT: 'immich/immich'

jobs:
  merge:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Generate a token
        id: generate_token
        if: ${{ inputs.skip != true }}
        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Find translation PR
        id: find_pr
        if: ${{ inputs.skip != true }}
        env:
          GH_TOKEN: ${{ steps.generate_token.outputs.token }}
        run: |
          set -euo pipefail

          PR=$(gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable)
          echo "$PR"

          PR_NUMBER=$(echo "$PR" | jq '
            if length == 1 then
              .[0].number
            else
              error("Expected exactly 1 entry, got \(length)")
            end
          ' 2>&1) || exit 1

          echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
          echo "Selected PR $PR_NUMBER"

          if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then
            echo "PR is not mergeable"
            exit 1
          fi

      - name: Lock weblate
        if: ${{ inputs.skip != true }}
        env:
          WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
        run: |
          curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=true

      - name: Commit translations
        if: ${{ inputs.skip != true }}
        env:
          WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
        run: |
          curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=commit
          curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=push

      - name: Merge PR
        id: merge_pr
        if: ${{ inputs.skip != true }}
        env:
          GH_TOKEN: ${{ steps.generate_token.outputs.token }}
          PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
        run: |
          set -euo pipefail

          REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
            | jq '.id')
          echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT
          gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash

      - name: Wait for PR to merge
        if: ${{ inputs.skip != true }}
        env:
          GH_TOKEN: ${{ steps.generate_token.outputs.token }}
          PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
          REVIEW_ID: ${{ steps.merge_pr.outputs.REVIEW_ID }}
        run: |
          # So we clean up no matter what
          set +e

          for i in {1..100}; do
            if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state | jq -e '.state == "MERGED"'; then
              echo "PR merged"
              exit 0
            else
              echo "PR not merged yet, waiting..."
              sleep 6
            fi
          done
          echo "PR did not merge in time"
          gh api -X PUT "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" --field message='Merge attempt timed out' --field event='DISMISS'
          gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --disable-auto
          exit 1

      - name: Unlock weblate
        if: ${{ inputs.skip != true }}
        env:
          WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
        run: |
          curl --fail-with-body -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=false

      - name: Report success
        run: |
          echo "Workflow completed successfully (or was skipped)"
org-pr-require-conventional-commit .github/workflows/org-pr-require-conventional-commit.yml
Triggers
pull_request
Runs on
Jobs
validate-pr-title
View raw YAML
name: PR Conventional Commit

on:
  pull_request:
    types: [opened, synchronize, reopened, edited]

jobs:
  validate-pr-title:
    name: Validate PR Title (conventional commit)
    uses: immich-app/devtools/.github/workflows/shared-pr-require-conventional-commit.yml@main
    permissions:
      pull-requests: write
org-zizmor .github/workflows/org-zizmor.yml
Triggers
pull_request, push
Runs on
Jobs
zizmor
View raw YAML
name: Zizmor

on:
  pull_request:
  push:
    branches: [main]

jobs:
  zizmor:
    name: Zizmor
    uses: immich-app/devtools/.github/workflows/shared-zizmor.yml@main
    permissions:
      actions: read
      contents: read
      security-events: write
pr-label-validation perms .github/workflows/pr-label-validation.yml
Triggers
pull_request_target
Runs on
ubuntu-latest
Jobs
validate-release-label
Actions
immich-app/devtools/actions/create-workflow-token, mheap/github-action-required-labels
View raw YAML
name: PR Label Validation

on:
  pull_request_target: # zizmor: ignore[dangerous-triggers] no attacker inputs are used here
    types: [opened, labeled, unlabeled, synchronize]

permissions: {}

jobs:
  validate-release-label:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Require PR to have a changelog label
        uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
        with:
          token: ${{ steps.token.outputs.token }}
          mode: exactly
          count: 1
          use_regex: true
          labels: 'changelog:.*'
          add_comment: true
          message: 'Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label.'
pr-labeler perms .github/workflows/pr-labeler.yml
Triggers
pull_request_target
Runs on
ubuntu-latest
Jobs
labeler
Actions
immich-app/devtools/actions/create-workflow-token, actions/labeler
View raw YAML
name: 'Pull Request Labeler'
on:
  - pull_request_target # zizmor: ignore[dangerous-triggers] no attacker inputs are used here

permissions: {}

jobs:
  labeler:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
        with:
          repo-token: ${{ steps.token.outputs.token }}
prepare-release perms .github/workflows/prepare-release.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest
Jobs
merge_translations, bump_version, build_mobile, prepare_release
Actions
actions/create-github-app-token, astral-sh/setup-uv, pnpm/action-setup, EndBug/add-and-commit, actions/create-github-app-token, softprops/action-gh-release
Commands
  • misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
  • echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
View raw YAML
name: Prepare new release

on:
  workflow_dispatch:
    inputs:
      serverBump:
        description: 'Bump server version'
        required: true
        default: 'false'
        type: choice
        options:
          - 'false'
          - major
          - minor
          - patch
      mobileBump:
        description: 'Bump mobile build number'
        required: false
        type: boolean
      skipTranslations:
        description: 'Skip translations'
        required: false
        type: boolean

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}-root
  cancel-in-progress: true

permissions: {}

jobs:
  merge_translations:
    uses: ./.github/workflows/merge-translations.yml
    with:
      skip: ${{ inputs.skipTranslations }}
    permissions:
      pull-requests: write
    secrets:
      PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
      PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
      WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}

  bump_version:
    runs-on: ubuntu-latest
    needs: [merge_translations]
    outputs:
      ref: ${{ steps.push-tag.outputs.commit_long_sha }}
      version: ${{ steps.output.outputs.version }}
    permissions: {} # No job-level permissions are needed because it uses the app-token
    steps:
      - name: Generate a token
        id: generate-token
        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          token: ${{ steps.generate-token.outputs.token }}
          persist-credentials: true
          ref: main

      - name: Install uv
        uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'

      - name: Bump version
        env:
          SERVER_BUMP: ${{ inputs.serverBump }}
          MOBILE_BUMP: ${{ inputs.mobileBump }}
        run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"

      - id: output
        run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT

      - name: Commit and tag
        id: push-tag
        uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
        with:
          default_author: github_actions
          message: 'chore: version ${{ steps.output.outputs.version }}'
          tag: ${{ steps.output.outputs.version }}
          push: true

  build_mobile:
    uses: ./.github/workflows/build-mobile.yml
    needs: bump_version
    permissions:
      contents: read
    secrets:
      KEY_JKS: ${{ secrets.KEY_JKS }}
      ALIAS: ${{ secrets.ALIAS }}
      ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
      ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
      # iOS secrets
      APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
      APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
      APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
      IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }}
      IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
      FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }}

    with:
      ref: ${{ needs.bump_version.outputs.ref }}
      environment: production

  prepare_release:
    runs-on: ubuntu-latest
    needs: [build_mobile, bump_version]
    permissions:
      actions: read # To download the app artifact
      # No content permissions are needed because it uses the app-token
    steps:
      - name: Generate a token
        id: generate-token
        uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          token: ${{ steps.generate-token.outputs.token }}
          persist-credentials: false

      - name: Download APK
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: release-apk-signed
          github-token: ${{ steps.generate-token.outputs.token }}

      - name: Create draft release
        uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
        with:
          draft: true
          tag_name: ${{ needs.bump_version.outputs.version }}
          token: ${{ steps.generate-token.outputs.token }}
          generate_release_notes: true
          body_path: misc/release/notes.tmpl
          files: |
            docker/docker-compose.yml
            docker/docker-compose.rootless.yml
            docker/example.env
            docker/hwaccel.ml.yml
            docker/hwaccel.transcoding.yml
            docker/prometheus.yml
            *.apk
preview-label perms .github/workflows/preview-label.yaml
Triggers
pull_request
Runs on
ubuntu-latest, ubuntu-latest
Jobs
comment-status, remove-label
Actions
immich-app/devtools/actions/create-workflow-token, mshick/add-pr-comment, immich-app/devtools/actions/create-workflow-token, mshick/add-pr-comment, mshick/add-pr-comment
View raw YAML
name: Preview label

on:
  pull_request:
    types: [labeled, closed]

permissions: {}

jobs:
  comment-status:
    runs-on: ubuntu-latest
    if: ${{ github.event.action == 'labeled' && github.event.label.name == 'preview' }}
    permissions:
      pull-requests: write
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
        with:
          github-token: ${{ steps.token.outputs.token }}
          message-id: 'preview-status'
          message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'

  remove-label:
    runs-on: ubuntu-latest
    if: ${{ (github.event.action == 'closed' || github.event.pull_request.head.repo.fork) && contains(github.event.pull_request.labels.*.name, 'preview') }}
    permissions:
      pull-requests: write
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        with:
          github-token: ${{ steps.token.outputs.token }}
          script: |
            github.rest.issues.removeLabel({
              issue_number: context.payload.pull_request.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              name: 'preview'
            })

      - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
        if: ${{ github.event.pull_request.head.repo.fork }}
        with:
          github-token: ${{ steps.token.outputs.token }}
          message-id: 'preview-status'
          message: 'PRs from forks cannot have preview environments.'

      - uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
        if: ${{ !github.event.pull_request.head.repo.fork }}
        with:
          github-token: ${{ steps.token.outputs.token }}
          message-id: 'preview-status'
          message: 'Preview environment has been removed.'
sdk perms .github/workflows/sdk.yml
Triggers
release
Runs on
ubuntu-latest
Jobs
publish
Actions
immich-app/devtools/actions/create-workflow-token, pnpm/action-setup
Commands
  • pnpm install --frozen-lockfile
  • pnpm build
  • pnpm publish --provenance --no-git-checks
View raw YAML
name: Update Immich SDK

on:
  release:
    types: [published]

permissions: {}

jobs:
  publish:
    name: Publish `@immich/sdk`
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
    defaults:
      run:
        working-directory: ./open-api/typescript-sdk
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

      # Setup .npmrc file to publish to npm
      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './open-api/typescript-sdk/.nvmrc'
          registry-url: 'https://registry.npmjs.org'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Install deps
        run: pnpm install --frozen-lockfile
      - name: Build
        run: pnpm build
      - name: Publish
        run: pnpm publish --provenance --no-git-checks
static_analysis perms .github/workflows/static_analysis.yml
Triggers
workflow_dispatch, pull_request, push
Runs on
ubuntu-latest, ubuntu-latest
Jobs
pre-job, mobile-dart-analyze
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, immich-app/devtools/actions/create-workflow-token, subosito/flutter-action, CQLabs/setup-dcm, tj-actions/verify-changed-files
Commands
  • dart pub get
  • dart pub get
  • dart pub get
  • dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
  • make build
  • make pigeon
  • echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory" echo "Changed files: ${CHANGED_FILES}" exit 1
  • dart analyze --fatal-infos
View raw YAML
name: Static Code Analysis
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: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          filters: |
            mobile:
              - 'mobile/**'
          force-filters: |
            - '.github/workflows/static_analysis.yml'
          force-events: 'workflow_dispatch,release'

  mobile-dart-analyze:
    name: Run Dart Code Analysis
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./mobile
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}

      - name: Setup Flutter SDK
        uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
        with:
          channel: 'stable'
          flutter-version-file: ./mobile/pubspec.yaml

      - name: Install dependencies
        run: dart pub get

      - name: Install dependencies for UI package
        run: dart pub get
        working-directory: ./mobile/packages/ui

      - name: Install dependencies for UI Showcase
        run: dart pub get
        working-directory: ./mobile/packages/ui/showcase

      - name: Install DCM
        uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
        with:
          github-token: ${{ steps.token.outputs.token }}
          version: auto
          working-directory: ./mobile

      - name: Generate translation file
        run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart

      - name: Run Build Runner
        run: make build

      - name: Generate platform API
        run: make pigeon

      - name: Find file changes
        uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
        id: verify-changed-files
        with:
          files: |
            mobile/**/*.g.dart
            mobile/**/*.gr.dart
            mobile/**/*.drift.dart

      - name: Verify files have not changed
        if: steps.verify-changed-files.outputs.files_changed == 'true'
        env:
          CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
        run: |
          echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory"
          echo "Changed files: ${CHANGED_FILES}"
          exit 1

      - name: Run dart analyze
        run: dart analyze --fatal-infos

      - name: Run dart format
        run: make format

      # TODO: Re-enable after upgrading custom_lint
      # - name: Run dart custom_lint
      #   run: dart run custom_lint

      # TODO: Use https://github.com/CQLabs/dcm-action
      - name: Run DCM
        run: dcm analyze lib --fatal-style --fatal-warnings
test matrix perms .github/workflows/test.yml
Triggers
workflow_dispatch, pull_request, push
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, windows-latest, mich, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ${{ matrix.runner }}, ${{ matrix.runner }}, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
pre-job, server-unit-tests, cli-unit-tests, cli-unit-tests-win, web-lint, web-unit-tests, i18n-tests, e2e-tests-lint, server-medium-tests, e2e-tests-server-cli, e2e-tests-web, success-check-e2e, mobile-unit-tests, ml-unit-tests, github-files-formatting, shellcheck, generated-api-up-to-date, sql-schema-up-to-date
Matrix
runner→ ubuntu-24.04-arm, ubuntu-latest
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, tj-actions/verify-changed-files, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/success-check, immich-app/devtools/actions/create-workflow-token, subosito/flutter-action, immich-app/devtools/actions/create-workflow-token, astral-sh/setup-uv, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, immich-app/devtools/actions/create-workflow-token, ludeeus/action-shellcheck, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, tj-actions/verify-changed-files, immich-app/devtools/actions/create-workflow-token, pnpm/action-setup, tj-actions/verify-changed-files, tj-actions/verify-changed-files
Commands
  • pnpm install
  • pnpm lint
  • pnpm format
  • pnpm check
  • pnpm test
  • pnpm install && pnpm run build
  • pnpm install
  • pnpm lint
View raw YAML
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: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          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/**'
            .github:
              - '.github/**'
          force-filters: |
            - '.github/workflows/test.yml'
          force-events: 'workflow_dispatch'

  server-unit-tests:
    name: Test & Lint Server
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./server
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}

      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run package manager install
        run: pnpm install
      - name: Run linter
        run: pnpm lint
        if: ${{ !cancelled() }}
      - name: Run formatter
        run: pnpm format
        if: ${{ !cancelled() }}
      - name: Run tsc
        run: pnpm check
        if: ${{ !cancelled() }}
      - name: Run small tests & coverage
        run: pnpm test
        if: ${{ !cancelled() }}
  cli-unit-tests:
    name: Unit Test CLI
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./cli
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './cli/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Setup typescript-sdk
        run: pnpm install && pnpm run build
        working-directory: ./open-api/typescript-sdk
      - name: Install deps
        run: pnpm install
      - name: Run linter
        run: pnpm lint
        if: ${{ !cancelled() }}
      - name: Run formatter
        run: pnpm format
        if: ${{ !cancelled() }}
      - name: Run tsc
        run: pnpm check
        if: ${{ !cancelled() }}
      - name: Run unit tests & coverage
        run: pnpm test
        if: ${{ !cancelled() }}
  cli-unit-tests-win:
    name: Unit Test CLI (Windows)
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).cli == true }}
    runs-on: windows-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./cli
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './cli/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
      - name: Install deps
        run: pnpm install --frozen-lockfile
      # Skip linter & formatter in Windows test.
      - name: Run tsc
        run: pnpm check
        if: ${{ !cancelled() }}
      - name: Run unit tests & coverage
        run: pnpm test
        if: ${{ !cancelled() }}
  web-lint:
    name: Lint Web
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }}
    runs-on: mich
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./web
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './web/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
      - name: Run pnpm install
        run: pnpm rebuild && pnpm install --frozen-lockfile
      - name: Run linter
        run: pnpm lint
        if: ${{ !cancelled() }}
      - name: Run formatter
        run: pnpm format
        if: ${{ !cancelled() }}
      - name: Run svelte checks
        run: pnpm check:svelte
        if: ${{ !cancelled() }}
  web-unit-tests:
    name: Test Web
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).web == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./web
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './web/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
      - name: Run npm install
        run: pnpm install --frozen-lockfile
      - name: Run tsc
        run: pnpm check:typescript
        if: ${{ !cancelled() }}
      - name: Run unit tests & coverage
        run: pnpm test
        if: ${{ !cancelled() }}
  i18n-tests:
    name: Test i18n
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './web/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Install dependencies
        run: pnpm --filter=immich-i18n install --frozen-lockfile
      - name: Format
        run: pnpm --filter=immich-i18n format:fix
      - 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: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./e2e
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './e2e/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
        if: ${{ !cancelled() }}
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        if: ${{ !cancelled() }}
      - name: Run linter
        run: pnpm lint
        if: ${{ !cancelled() }}
      - name: Run formatter
        run: pnpm format
        if: ${{ !cancelled() }}
      - name: Run tsc
        run: pnpm check
        if: ${{ !cancelled() }}
  server-medium-tests:
    name: Medium Tests (Server)
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./server
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run pnpm install
        run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
      - name: Run medium tests
        run: pnpm test:medium
        if: ${{ !cancelled() }}
  e2e-tests-server-cli:
    name: End-to-End Tests (Server & CLI)
    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
    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@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './e2e/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
        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
        run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
        if: ${{ !cancelled() }}
      - name: Run e2e tests (api & cli)
        env:
          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() }}
      - name: Capture Docker logs
        if: always()
        run: docker compose logs --no-color > docker-compose-logs.txt
        working-directory: ./e2e
      - name: Archive Docker logs
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: always()
        with:
          name: e2e-server-docker-logs-${{ matrix.runner }}
          path: e2e/docker-compose-logs.txt
  e2e-tests-web:
    name: End-to-End Tests (Web)
    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
    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@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './e2e/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run setup typescript-sdk
        run: pnpm install --frozen-lockfile && pnpm build
        working-directory: ./open-api/typescript-sdk
        if: ${{ !cancelled() }}
      - name: Install dependencies
        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() }}
      - name: Run e2e tests (web)
        env:
          PLAYWRIGHT_DISABLE_WEBSERVER: true
        run: pnpm test:web
        if: ${{ !cancelled() }}
      - name: Archive e2e test (web) results
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: success() || failure()
        with:
          name: e2e-web-test-results-${{ matrix.runner }}
          path: e2e/playwright-report/
      - name: Run ui tests (web)
        env:
          PLAYWRIGHT_DISABLE_WEBSERVER: true
        run: pnpm test:web:ui
        if: ${{ !cancelled() }}
      - name: Archive ui test (web) results
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: success() || failure()
        with:
          name: e2e-ui-test-results-${{ matrix.runner }}
          path: e2e/playwright-report/
      - name: Run maintenance tests
        env:
          PLAYWRIGHT_DISABLE_WEBSERVER: true
        run: pnpm test:web:maintenance
        if: ${{ !cancelled() }}
      - name: Archive maintenance tests (web) results
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: success() || failure()
        with:
          name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
          path: e2e/playwright-report/
      - name: Capture Docker logs
        if: always()
        run: docker compose logs --no-color > docker-compose-logs.txt
        working-directory: ./e2e
      - name: Archive Docker logs
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        if: always()
        with:
          name: e2e-web-docker-logs-${{ matrix.runner }}
          path: e2e/docker-compose-logs.txt
  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@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
        with:
          needs: ${{ toJSON(needs) }}
  mobile-unit-tests:
    name: Unit Test Mobile
    needs: pre-job
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}
      - name: Setup Flutter SDK
        uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
        with:
          channel: 'stable'
          flutter-version-file: ./mobile/pubspec.yaml
      - name: Generate translation file
        run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
        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: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./machine-learning
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}
      - name: Install uv
        uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
        with:
          python-version: 3.11
      - name: Install dependencies
        run: |
          uv sync --extra cpu
      - name: Lint with ruff
        run: |
          uv run ruff check --output-format=github immich_ml
      - name: Format with ruff
        run: |
          uv run ruff format --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: ${{ fromJSON(needs.pre-job.outputs.should_run)['.github'] == true }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    defaults:
      run:
        working-directory: ./.github
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './.github/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Run pnpm install
        run: pnpm install --frozen-lockfile
      - name: Run formatter
        run: pnpm format
        if: ${{ !cancelled() }}
  shellcheck:
    name: ShellCheck
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
          token: ${{ steps.token.outputs.token }}
      - 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:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Install server dependencies
        run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
      - name: Build the app
        run: pnpm --filter immich build
      - name: Run API generation
        run: ./bin/generate-open-api.sh
        working-directory: 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:dbf18b3ffea4a81434c65b71e20d27203baf903a0275f4341e4c16dfd901fd67
        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:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        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
          token: ${{ steps.token.outputs.token }}
      - name: Setup pnpm
        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
      - name: Setup Node
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
        with:
          node-version-file: './server/.nvmrc'
          cache: 'pnpm'
          cache-dependency-path: '**/pnpm-lock.yaml'
      - name: Install server dependencies
        run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
      - name: Build the app
        run: pnpm build
      - name: Run existing migrations
        run: pnpm migrations:run
      - name: Test npm run schema:reset command works
        run: pnpm schema:reset
      - name: Generate new migrations
        continue-on-error: true
        run: pnpm 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: pnpm 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
weblate-lock perms .github/workflows/weblate-lock.yml
Triggers
pull_request
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
pre-job, enforce-lock, success-check-lock
Actions
immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/pre-job, immich-app/devtools/actions/create-workflow-token, immich-app/devtools/actions/success-check
Commands
  • # Then check for APPROVED by the bot, if absent fail gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == env.BOT_NAME and .state == "APPROVED")) | length > 0' \ || (echo "The push-o-matic bot has not approved this PR yet" && exit 1)
View raw YAML
name: Weblate checks

on:
  pull_request:
    branches: [main]
    types:
      - opened
      - synchronize
      - ready_for_review
      - auto_merge_enabled
      - auto_merge_disabled

permissions: {}

env:
  BOT_NAME: immich-push-o-matic

jobs:
  pre-job:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Check what should run
        id: check
        uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
        with:
          github-token: ${{ steps.token.outputs.token }}
          filters: |
            i18n:
              - modified: 'i18n/!(en|package)**\.json'
          skip-force-logic: 'true'

  enforce-lock:
    name: Check Weblate Lock
    needs: [pre-job]
    runs-on: ubuntu-latest
    permissions: {}
    if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
    steps:
      - id: token
        uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
        with:
          app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
          private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

      - name: Bot review status
        env:
          PR_NUMBER: ${{ github.event.pull_request.number || github.event.pull_request_review.pull_request.number }}
          GH_TOKEN: ${{ steps.token.outputs.token }}
        run: |
          # Then check for APPROVED by the bot, if absent fail
          gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == env.BOT_NAME and .state == "APPROVED")) | length > 0' \
            || (echo "The push-o-matic bot has not approved this PR yet" && exit 1)

  success-check-lock:
    name: Weblate Lock Check Success
    needs: [enforce-lock]
    runs-on: ubuntu-latest
    permissions: {}
    if: always()
    steps:
      - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
        with:
          needs: ${{ toJSON(needs) }}