name: Release on: release: types: [published] workflow_dispatch: inputs: version_tag: description: 'Version tag for Docker images (e.g., 0.26.2). Auto-detected from release/* branch name if empty.' required: false type: string tag_latest: description: 'Also tag images as :latest' required: false default: false type: boolean build_browser_extensions: description: 'Build browser extensions' required: false default: true type: boolean build_mobile_apps: description: 'Build mobile apps' required: false default: true type: boolean build_multi_container: description: 'Build and push multi-container images' required: false default: true type: boolean build_all_in_one: description: 'Build and push all-in-one image' required: false default: true type: boolean jobs: # Guard job to prevent releases from main branch valid-release: if: github.event_name == 'release' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check tag target run: | BRANCHES=$(git branch -r --contains $GITHUB_SHA) echo "Tag is contained in:" echo "$BRANCHES" if ! echo "$BRANCHES" | grep -q "origin/release/"; then echo "❌ Releases must come from a release/* branch, please recreate the release from a release branch" exit 1 fi echo "✅ Tag is on a release branch" # Detect version from input, branch name, or release tag detect-version: if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') needs: [valid-release] runs-on: ubuntu-latest outputs: version: ${{ steps.detect.outputs.version }} tag_latest: ${{ steps.detect.outputs.tag_latest }} all_in_one_exists: ${{ steps.check-images.outputs.all_in_one_exists }} multi_container_exists: ${{ steps.check-images.outputs.multi_container_exists }} browser_extension_version_matches: ${{ steps.check-app-versions.outputs.browser_extension_matches }} mobile_app_version_matches: ${{ steps.check-app-versions.outputs.mobile_app_matches }} steps: - name: Checkout repository uses: actions/checkout@v4 with: sparse-checkout: | apps/browser-extension/package.json apps/mobile-app/app.json - name: Detect version id: detect run: | # Priority: 1) input version_tag, 2) release tag, 3) branch name pattern if [ -n "${{ inputs.version_tag }}" ]; then VERSION="${{ inputs.version_tag }}" echo "Using input version_tag: $VERSION" elif [ "${{ github.event_name }}" = "release" ]; then VERSION="${{ github.event.release.tag_name }}" echo "Using release tag: $VERSION" elif [[ "${{ github.ref_name }}" =~ ^release/(.+)$ ]]; then VERSION="${BASH_REMATCH[1]}" echo "Extracted version from branch name: $VERSION" else VERSION="" echo "No version detected, will use branch-based tags" fi # Normalize version: strip leading 'v' if present for consistent Docker tags VERSION="${VERSION#v}" echo "Normalized version: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT # Determine if we should tag as latest if [ "${{ github.event_name }}" = "release" ] && [ "${{ github.event.release.prerelease }}" = "false" ]; then echo "tag_latest=true" >> $GITHUB_OUTPUT elif [ "${{ inputs.tag_latest }}" = "true" ]; then echo "tag_latest=true" >> $GITHUB_OUTPUT else echo "tag_latest=false" >> $GITHUB_OUTPUT fi - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Check if Docker images already exist id: check-images run: | VERSION="${{ steps.detect.outputs.version }}" if [ -z "$VERSION" ]; then echo "all_in_one_exists=false" >> $GITHUB_OUTPUT echo "multi_container_exists=false" >> $GITHUB_OUTPUT echo "No version specified, images will be built" exit 0 fi # Check all-in-one image if docker manifest inspect ghcr.io/aliasvault/aliasvault:${VERSION} > /dev/null 2>&1; then echo "all_in_one_exists=true" >> $GITHUB_OUTPUT echo "✅ All-in-one image ghcr.io/aliasvault/aliasvault:${VERSION} exists" else echo "all_in_one_exists=false" >> $GITHUB_OUTPUT echo "❌ All-in-one image does not exist" fi # Check multi-container images (check a few key ones) MULTI_IMAGES=( "ghcr.io/aliasvault/api" "ghcr.io/aliasvault/client" "ghcr.io/aliasvault/admin" ) MULTI_EXISTS=true for IMAGE in "${MULTI_IMAGES[@]}"; do if ! docker manifest inspect ${IMAGE}:${VERSION} > /dev/null 2>&1; then echo "❌ Multi-container image ${IMAGE}:${VERSION} does not exist" MULTI_EXISTS=false break fi echo "✅ ${IMAGE}:${VERSION} exists" done echo "multi_container_exists=${MULTI_EXISTS}" >> $GITHUB_OUTPUT - name: Check app versions match release tag id: check-app-versions run: | VERSION="${{ steps.detect.outputs.version }}" if [ -z "$VERSION" ]; then # No version tag, allow builds (workflow_dispatch without version) echo "browser_extension_matches=true" >> $GITHUB_OUTPUT echo "mobile_app_matches=true" >> $GITHUB_OUTPUT echo "No release version specified, all builds allowed" exit 0 fi # Check browser extension version BROWSER_EXT_VERSION=$(node -p "require('./apps/browser-extension/package.json').version") echo "Browser extension version: $BROWSER_EXT_VERSION" echo "Release version: $VERSION" if [ "$BROWSER_EXT_VERSION" = "$VERSION" ]; then echo "✅ Browser extension version matches release tag" echo "browser_extension_matches=true" >> $GITHUB_OUTPUT else echo "⚠️ Browser extension version ($BROWSER_EXT_VERSION) does not match release tag ($VERSION)" echo "Browser extensions will NOT be uploaded to release" echo "browser_extension_matches=false" >> $GITHUB_OUTPUT fi # Check mobile app version MOBILE_APP_VERSION=$(node -p "require('./apps/mobile-app/app.json').expo.version") echo "Mobile app version: $MOBILE_APP_VERSION" if [ "$MOBILE_APP_VERSION" = "$VERSION" ]; then echo "✅ Mobile app version matches release tag" echo "mobile_app_matches=true" >> $GITHUB_OUTPUT else echo "⚠️ Mobile app version ($MOBILE_APP_VERSION) does not match release tag ($VERSION)" echo "Android APK will NOT be uploaded to release" echo "mobile_app_matches=false" >> $GITHUB_OUTPUT fi upload-install-script: needs: [valid-release, detect-version] if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Upload install.sh to release if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: files: install.sh token: ${{ secrets.GITHUB_TOKEN }} build-chrome-extension: needs: [valid-release, detect-version] if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_browser_extensions) && needs.detect-version.outputs.browser_extension_version_matches == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build Chrome Extension uses: ./.github/actions/build-browser-extension with: browser: chrome upload_to_release: ${{ github.event_name == 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-firefox-extension: needs: [valid-release, detect-version] if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_browser_extensions) && needs.detect-version.outputs.browser_extension_version_matches == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build Firefox Extension uses: ./.github/actions/build-browser-extension with: browser: firefox upload_to_release: ${{ github.event_name == 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-edge-extension: needs: [valid-release, detect-version] if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_browser_extensions) && needs.detect-version.outputs.browser_extension_version_matches == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build Edge Extension uses: ./.github/actions/build-browser-extension with: browser: edge upload_to_release: ${{ github.event_name == 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-android-release: needs: [valid-release, detect-version] if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_mobile_apps) && needs.detect-version.outputs.mobile_app_version_matches == 'true' runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build Android App uses: ./.github/actions/build-android-app with: signed: true upload_to_release: ${{ github.event_name == 'release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} build-and-push-docker-multi-container: needs: [valid-release, detect-version] # Skip if images already exist during release (just tag them as latest instead), but allow manual rebuild via workflow_dispatch if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_multi_container) && (github.event_name != 'release' || needs.detect-version.outputs.multi_container_exists != 'true') runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Postgres image id: postgres-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/postgres tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault PostgreSQL org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for API image id: api-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/api tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault API org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for Client image id: client-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/client tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault Client org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for Admin image id: admin-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/admin tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault Admin org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for Reverse Proxy image id: reverse-proxy-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/reverse-proxy tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault Reverse Proxy org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for SMTP image id: smtp-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/smtp tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault SMTP Service org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for TaskRunner image id: task-runner-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/task-runner tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault TaskRunner org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) annotations: | org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net) - name: Extract metadata for InstallCLI image id: installcli-meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ghcr.io/aliasvault/installcli tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault Install CLI org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack annotations: | org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack - name: Build and push Postgres image uses: docker/build-push-action@v6 with: context: . file: apps/server/Databases/AliasServerDb/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.postgres-meta.outputs.tags }} labels: ${{ steps.postgres-meta.outputs.labels }} annotations: ${{ steps.postgres-meta.outputs.annotations }} - name: Build and push API image uses: docker/build-push-action@v6 with: context: . file: apps/server/AliasVault.Api/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.api-meta.outputs.tags }} labels: ${{ steps.api-meta.outputs.labels }} annotations: ${{ steps.api-meta.outputs.annotations }} - name: Build and push Client image uses: docker/build-push-action@v6 with: context: . file: apps/server/AliasVault.Client/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.client-meta.outputs.tags }} labels: ${{ steps.client-meta.outputs.labels }} annotations: ${{ steps.client-meta.outputs.annotations }} - name: Build and push Admin image uses: docker/build-push-action@v6 with: context: . file: apps/server/AliasVault.Admin/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.admin-meta.outputs.tags }} labels: ${{ steps.admin-meta.outputs.labels }} annotations: ${{ steps.admin-meta.outputs.annotations }} - name: Build and push Reverse Proxy image uses: docker/build-push-action@v6 with: context: . file: apps/server/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.reverse-proxy-meta.outputs.tags }} labels: ${{ steps.reverse-proxy-meta.outputs.labels }} annotations: ${{ steps.reverse-proxy-meta.outputs.annotations }} - name: Build and push SMTP image uses: docker/build-push-action@v6 with: context: . file: apps/server/Services/AliasVault.SmtpService/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.smtp-meta.outputs.tags }} labels: ${{ steps.smtp-meta.outputs.labels }} annotations: ${{ steps.smtp-meta.outputs.annotations }} - name: Build and push TaskRunner image uses: docker/build-push-action@v6 with: context: . file: apps/server/Services/AliasVault.TaskRunner/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.task-runner-meta.outputs.tags }} labels: ${{ steps.task-runner-meta.outputs.labels }} annotations: ${{ steps.task-runner-meta.outputs.annotations }} - name: Build and push InstallCli image uses: docker/build-push-action@v6 with: context: . file: apps/server/Utilities/AliasVault.InstallCli/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.installcli-meta.outputs.tags }} labels: ${{ steps.installcli-meta.outputs.labels }} annotations: ${{ steps.installcli-meta.outputs.annotations }} build-and-push-docker-all-in-one: needs: [valid-release, detect-version] # Skip if images already exist during release (just tag them as latest instead), but allow manual rebuild via workflow_dispatch if: always() && (github.event_name != 'release' || needs.valid-release.result == 'success') && (github.event_name == 'release' || inputs.build_all_in_one) && (github.event_name != 'release' || needs.detect-version.outputs.all_in_one_exists != 'true') runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata for all-in-one image id: meta uses: docker/metadata-action@v5 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: | ghcr.io/aliasvault/aliasvault aliasvault/aliasvault tags: | type=raw,value=${{ needs.detect-version.outputs.version }},enable=${{ needs.detect-version.outputs.version != '' }} type=raw,value=latest,enable=${{ needs.detect-version.outputs.tag_latest == 'true' }} type=ref,event=branch,enable=${{ needs.detect-version.outputs.version == '' }} type=raw,value={{branch}}-{{sha}},enable=${{ needs.detect-version.outputs.version == '' }} labels: | org.opencontainers.image.title=AliasVault All-in-One org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net). annotations: | org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net). - name: Build and push all-in-one image uses: docker/build-push-action@v6 with: context: . file: dockerfiles/all-in-one/Dockerfile platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} # Tag existing images as :latest without rebuilding (for release events when images already exist) tag-docker-images-latest: needs: [valid-release, detect-version] if: github.event_name == 'release' && needs.valid-release.result == 'success' && needs.detect-version.outputs.tag_latest == 'true' runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Tag all-in-one images as latest if: needs.detect-version.outputs.all_in_one_exists == 'true' run: | VERSION="${{ needs.detect-version.outputs.version }}" echo "Tagging ghcr.io/aliasvault/aliasvault:${VERSION} as latest..." docker buildx imagetools create -t ghcr.io/aliasvault/aliasvault:latest ghcr.io/aliasvault/aliasvault:${VERSION} docker buildx imagetools create -t aliasvault/aliasvault:latest aliasvault/aliasvault:${VERSION} - name: Tag multi-container images as latest if: needs.detect-version.outputs.multi_container_exists == 'true' run: | VERSION="${{ needs.detect-version.outputs.version }}" IMAGES=( "ghcr.io/aliasvault/postgres" "ghcr.io/aliasvault/api" "ghcr.io/aliasvault/client" "ghcr.io/aliasvault/admin" "ghcr.io/aliasvault/reverse-proxy" "ghcr.io/aliasvault/smtp" "ghcr.io/aliasvault/task-runner" "ghcr.io/aliasvault/installcli" ) for IMAGE in "${IMAGES[@]}"; do if docker manifest inspect ${IMAGE}:${VERSION} > /dev/null 2>&1; then echo "Tagging ${IMAGE}:${VERSION} as latest..." docker buildx imagetools create -t ${IMAGE}:latest ${IMAGE}:${VERSION} else echo "⚠️ Skipping ${IMAGE}:${VERSION} (does not exist)" fi done