This page documents the GitHub Actions workflows that automate testing, building, publishing, and deploying Immich. It covers every workflow file under .github/workflows/, including how they are triggered, what jobs they run, and how they relate to each other.
For information about the local development build system (mise tasks, pnpm scripts), see Build System and Tooling. For information about the testing strategies used within these workflows, see Testing Framework.
All workflows live under .github/workflows/. The table below lists each file, its trigger conditions, and its primary purpose.
| Workflow file | Triggers | Purpose |
|---|---|---|
test.yml | PR, push to main, manual | Lint, type-check, and unit/medium/e2e tests for all components |
docker.yml | PR, push to main, release, manual | Build and publish Docker images to GHCR (and Docker Hub on release) |
cli.yml | PR, push to main, release | Build, publish CLI to npm, push CLI Docker image |
sdk.yml | Release | Publish @immich/sdk TypeScript package to npm |
build-mobile.yml | PR, push to main, workflow_call | Build and sign Android APK; build and deploy iOS to TestFlight |
prepare-release.yml | Manual (workflow_dispatch) | Orchestrate full release: translations, version bump, mobile build, draft release |
static_analysis.yml | PR, push to main, manual | Dart static analysis and DCM checks on the mobile app |
docs-build.yml | PR, push to main, release | Build Docusaurus documentation site |
docs-deploy.yml | workflow_run (after docs-build) | Deploy built docs to Cloudflare Pages |
docs-destroy.yml | PR closed | Tear down PR-specific docs preview deployment |
codeql-analysis.yml | Push to main, PR, weekly cron | GitHub CodeQL security scanning (JavaScript, Python) |
weblate-lock.yml | PR opened/updated | Enforce that i18n-modifying PRs have bot approval |
fix-format.yml | PR labeled fix:formatting | Auto-commit formatting fixes to a PR branch |
close-duplicates.yml | Issue opened, Discussion created | Auto-close issues/discussions lacking duplicate search confirmation |
cache-cleanup.yml | PR closed | Delete GitHub Actions caches for the closed branch |
Sources: .github/workflows/test.yml1-10 .github/workflows/docker.yml1-14 .github/workflows/cli.yml1-14 .github/workflows/sdk.yml1-6 .github/workflows/build-mobile.yml1-43 .github/workflows/prepare-release.yml1-30 .github/workflows/static_analysis.yml1-12 .github/workflows/docs-build.yml1-13 .github/workflows/docs-deploy.yml1-11 .github/workflows/docs-destroy.yml1-10 .github/workflows/codeql-analysis.yml1-28 .github/workflows/weblate-lock.yml1-13 .github/workflows/fix-format.yml1-8 .github/workflows/close-duplicates.yml1-8 .github/workflows/cache-cleanup.yml1-11
pre-job PatternAlmost every primary workflow begins with a pre-job job that uses immich-app/devtools/actions/pre-job. This action examines the set of changed files in the current push or PR and outputs a JSON object (should_run) indicating which component-specific jobs should execute. Subsequent jobs use if: ${{ fromJSON(needs.pre-job.outputs.should_run).<component> == true }} to skip jobs that are not relevant to the change.
This avoids running the full test matrix for every commit. For example, a change only to machine-learning/ will skip server tests, web tests, and mobile tests entirely.
Path filters defined in test.yml:
| Filter key | Paths watched |
|---|---|
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 and force-events ensure that certain workflow files (test.yml itself, docker.yml, etc.) always force a full run when they are changed, and that workflow_dispatch triggers always run everything.
Sources: .github/workflows/test.yml17-53 .github/workflows/docker.yml31-48 .github/workflows/build-mobile.yml58-70
PUSH_O_MATIC GitHub AppNearly every job starts by calling immich-app/devtools/actions/create-workflow-token with PUSH_O_MATIC_APP_ID and PUSH_O_MATIC_APP_KEY secrets. This generates a short-lived GitHub App token (steps.token.outputs.token) used to check out code and push commits, bypassing the default GITHUB_TOKEN permission limitations.
Sources: .github/workflows/test.yml19-29 .github/workflows/prepare-release.yml51-56
Every primary workflow defines a concurrency group of ${{ github.workflow }}-${{ github.ref }} with cancel-in-progress: true. This cancels any running instance of the same workflow on the same branch when a new push arrives.
Sources: .github/workflows/test.yml7-9 .github/workflows/docker.yml11-13
test.yml)Trigger: Pull request, push to main, or workflow_dispatch.
Overview of the pre-job → job dependency:
Sources: .github/workflows/test.yml54-829
| Job | Runner | Key Steps |
|---|---|---|
server-unit-tests | ubuntu-latest | pnpm lint, pnpm format, pnpm check (tsc), pnpm test |
server-medium-tests | ubuntu-latest | pnpm test:medium (requires submodules) |
cli-unit-tests | ubuntu-latest | Builds typescript-sdk first, then pnpm lint, pnpm format, pnpm check, pnpm test |
cli-unit-tests-win | windows-latest | Same as above but skips lint/format; tests Windows path handling |
web-lint | mich (self-hosted) | typescript-sdk build, pnpm lint, pnpm format, pnpm check:svelte |
web-unit-tests | ubuntu-latest | pnpm check:typescript, pnpm test |
i18n-tests | ubuntu-latest | pnpm --filter=immich-i18n format:fix, then verifies no files changed |
e2e-tests-lint | ubuntu-latest | pnpm lint, pnpm format, pnpm check for e2e/ |
e2e-tests-server-cli | matrix: ubuntu-latest, ubuntu-24.04-arm | Docker Compose up, pnpm test, pnpm test:maintenance; archives Docker logs |
e2e-tests-web | matrix: ubuntu-latest, ubuntu-24.04-arm | Playwright Chromium install, Docker Compose up, pnpm test:web, pnpm test:web:ui, pnpm test:web:maintenance; archives Playwright reports |
mobile-unit-tests | ubuntu-latest | Flutter stable, dart run easy_localization:generate, flutter test -j 1 |
ml-unit-tests | ubuntu-latest | uv sync --extra cpu, ruff check, ruff format, mypy, pytest --cov |
github-files-formatting | ubuntu-latest | pnpm format on .github/ files |
shellcheck | ubuntu-latest | ludeeus/action-shellcheck (excludes open-api/, node_modules/) |
generated-api-up-to-date | ubuntu-latest | Builds server, runs ./bin/generate-open-api.sh, verifies no changes to mobile/openapi, open-api/typescript-sdk, open-api/immich-openapi-specs.json |
sql-schema-up-to-date | ubuntu-latest | Runs with a live Postgres service; runs migrations, resets schema, attempts migration generation, runs pnpm sync:sql, verifies no drift |
Sources: .github/workflows/test.yml54-828
docker.yml)Trigger: Pull request, push to main, release publication, or workflow_dispatch.
Published images (to ghcr.io/immich-app/):
| Image | Tags |
|---|---|
immich-server | main, pr-<N>, commit-<sha>, version tag on release |
immich-machine-learning | Same, plus suffixes: -cuda, -rocm, -openvino, -armnn, -rknn |
Re-tag strategy: When only the server (or ML) component has not changed but a release or PR needs a new tag, the retag_server / retag_ml jobs use docker buildx imagetools create to re-tag the existing main image instead of rebuilding it. This avoids redundant multi-hour builds.
Machine learning build matrix:
device | Suffix | Platforms |
|---|---|---|
cpu | (none) | linux/amd64, linux/arm64 |
cuda | -cuda | linux/amd64 |
openvino | -openvino | linux/amd64 |
armnn | -armnn | linux/arm64 |
rknn | -rknn | linux/arm64 |
rocm | -rocm | linux/amd64 (uses pokedex-giant self-hosted runner) |
All builds are delegated to the reusable workflow immich-app/devtools/.github/workflows/multi-runner-build.yml. DockerHub push (dockerhub-push) is only enabled on release events.
Docker image tag scheme:
Sources: .github/workflows/docker.yml50-194
cli.yml)Trigger: Push to main (paths cli/**), pull request, or release.
Jobs:
publish — Builds the CLI (pnpm build) and publishes to npm (pnpm publish --provenance) on release. The open-api/typescript-sdk package is always built as a dependency first.docker — Builds a multi-arch (linux/amd64, linux/arm64) Docker image from cli/Dockerfile. On release, pushes image tagged with the package version and latest to ghcr.io/immich-app/immich-cli.Sources: .github/workflows/cli.yml1-127
sdk.yml)Trigger: Release publication only.
Single job (publish): Builds open-api/typescript-sdk and runs pnpm publish --provenance to publish @immich/sdk to npm. The workflow uses id-token: write permission to enable npm provenance attestation.
Sources: .github/workflows/sdk.yml1-48
build-mobile.yml)Trigger: Push to main, pull request, or workflow_call (used by prepare-release.yml).
Jobs:
build-sign-androidmich runner.KEY_JKS secret (base64-encoded keystore) into android/key.jks..dart_tool caches keyed to build-mobile-gradle-<OS>-main.dart run easy_localization:generate, dart run bin/generate_keys.dart).make pigeon.main branch: builds a full release APK and split-ABI release APKs.release-apk-signed.main only.build-sign-iosmacos-15, selects Xcode 26.pod install.APP_STORE_CONNECT_API_KEY secret.build.keychain.gha_testflight_dev — main branch, development environment.gha_release_prod — main branch, production environment (called from release workflow).gha_build_only — all other branches; does not upload.Runner.ipa as artifact ios-release-ipa.Sources: .github/workflows/build-mobile.yml71-297
prepare-release.yml)Trigger: workflow_dispatch only. Inputs:
| Input | Type | Purpose |
|---|---|---|
serverBump | choice (false, major, minor, patch) | Which semver component to bump |
mobileBump | boolean | Whether to increment the mobile build number |
skipTranslations | boolean | Skip the translation merge step |
Release pipeline sequence:
The draft release includes the following files:
docker/docker-compose.ymldocker/example.envdocker/hwaccel.ml.ymldocker/hwaccel.transcoding.ymldocker/prometheus.yml*.apk (from the Android build artifact)After the draft is reviewed and published manually, the docker.yml, cli.yml, and sdk.yml workflows are all triggered by the release: [published] event and handle Docker image tagging, npm publishing, and DockerHub pushes.
Sources: .github/workflows/prepare-release.yml1-158
The documentation system uses a three-workflow design to safely handle Cloudflare Pages deployments from PR contexts.
docs-build.ymlTrigger: Push to main, pull request, release.
The pre-job filters on changes to docs/** or open-api/immich-openapi-specs.json. The single build job:
pnpm install + pnpm format (checks formatting) + pnpm build in ./docs.docs-build-output (retained for 1 day).Sources: .github/workflows/docs-build.yml1-94
docs-deploy.ymlTrigger: workflow_run (after Docs build completes successfully).
This indirect trigger pattern avoids exposing secrets to PR builds from forks, since the deploy job only runs in the context of the base repository.
The workflow:
docs-build-output artifact from the triggering workflow run.main subdomain; PR → pr-<N> subdomain; release → release subdomain).mise run //deployment:tf apply) to provision a Cloudflare Pages project subdomain.mise run //docs:deploy.docs-release Terraform module.actions-cool/maintain-one-comment.Sources: .github/workflows/docs-deploy.yml1-222
docs-destroy.ymlTrigger: pull_request_target closed.
Runs mise run //deployment:tf destroy against the pr-<N> Cloudflare infrastructure and removes the PR comment that contained the preview URL.
Sources: .github/workflows/docs-destroy.yml1-51
static_analysis.ymlTrigger: PR, push to main, manual.
Runs only when mobile/** changes. Steps:
make build (build_runner code gen).make pigeon (platform API generation)..g.dart, .gr.dart, or .drift.dart files have drifted from what is committed.dart analyze --fatal-infosmake formatdcm analyze lib --fatal-style --fatal-warnings using DCM (Dart Code Metrics).Sources: .github/workflows/static_analysis.yml40-126
codeql-analysis.ymlTrigger: Push to main, PR against main, weekly (Monday 13:20 UTC).
Runs GitHub's CodeQL engine against javascript and python in a matrix. Uses autobuild for dependency inference. Results appear in the GitHub Security tab.
Sources: .github/workflows/codeql-analysis.yml29-87
weblate-lock.ymlTrigger: PR opened, synchronized, or reviewed (against main).
Watches for modifications to any non-English i18n files (i18n/!(en|package)**\.json). If such files are changed, requires that the immich-push-o-matic bot has left an APPROVED review on the PR. This prevents manual edits to community-managed translation files without bot acknowledgment.
Sources: .github/workflows/weblate-lock.yml1-73
fix-format.ymlTrigger: Pull request labeled with fix:formatting.
Runs pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix across all packages, commits the changes using EndBug/add-and-commit, and removes the label.
Sources: .github/workflows/fix-format.yml1-62
close-duplicates.ymlTrigger: Issue opened, Feature Request discussion created.
Parses the issue/discussion body for a checked "I have searched for duplicates" checkbox using a custom mdq container image. If the checkbox is not checked, automatically closes the issue or discussion with a duplicate warning comment.
Sources: .github/workflows/close-duplicates.yml1-108
cache-cleanup.ymlTrigger: Pull request closed.
Uses gh extension install actions/gh-actions-cache to enumerate and delete all GitHub Actions caches associated with the PR's ref. Prevents cache bloat accumulation from merged or abandoned PRs.
Sources: .github/workflows/cache-cleanup.yml1-53
The following diagram shows which workflow is responsible for each component's CI path.
Sources: .github/workflows/test.yml1-10 .github/workflows/docker.yml1-14 .github/workflows/cli.yml1-14 .github/workflows/sdk.yml1-6 .github/workflows/build-mobile.yml1-43
Sources: .github/workflows/prepare-release.yml1-158 .github/workflows/docker.yml1-14 .github/workflows/cli.yml1-14 .github/workflows/sdk.yml1-6 .github/workflows/docs-build.yml1-13 .github/workflows/docs-deploy.yml1-11
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.