--- name: 'merge backend manifest list (reusable)' # Reusable workflow that joins per-arch digest artifacts (uploaded by # backend_build.yml when called with platform-tag) into a single tagged # multi-arch manifest list. Called once per backend by backend.yml after # both per-arch build jobs succeed. on: workflow_call: inputs: tag-latest: description: 'Whether the manifest list should also be tagged latest (auto/false/true)' required: false type: string default: '' tag-suffix: description: 'Backend tag suffix (e.g. -cpu-faster-whisper). Used to compute the artifact pattern and the final tag suffix.' required: true type: string secrets: dockerUsername: required: false dockerPassword: required: false quayUsername: required: true quayPassword: required: true jobs: merge: runs-on: ubuntu-latest env: quay_username: ${{ secrets.quayUsername }} steps: # Sparse checkout: the merge job needs `.github/scripts/` (for the # keepalive cleanup script) but none of the source tree. - name: Checkout (.github/scripts only) uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts sparse-checkout-cone-mode: false # `--` separator anchors the glob so we don't over-match sibling # backends whose tag-suffix happens to be a prefix of ours # (e.g. -cpu-vllm vs -cpu-vllm-omni). Must stay in sync with the # upload-artifact name in backend_build.yml. - name: Download digests uses: actions/download-artifact@v8 with: pattern: digests${{ inputs.tag-suffix }}--* merge-multiple: true path: /tmp/digests - name: Set up Docker Buildx uses: docker/setup-buildx-action@master - name: Login to DockerHub if: github.event_name != 'pull_request' uses: docker/login-action@v4 with: username: ${{ secrets.dockerUsername }} password: ${{ secrets.dockerPassword }} - name: Login to Quay.io if: ${{ env.quay_username != '' }} uses: docker/login-action@v4 with: registry: quay.io username: ${{ secrets.quayUsername }} password: ${{ secrets.quayPassword }} - name: Docker meta id: meta if: github.event_name != 'pull_request' uses: docker/metadata-action@v6 with: images: | quay.io/go-skynet/local-ai-backends localai/localai-backends tags: | type=ref,event=branch type=semver,pattern={{raw}} type=sha flavor: | latest=${{ inputs.tag-latest }} suffix=${{ inputs.tag-suffix }},onlatest=true # Source from ci-cache, not local-ai-backends. # # The build job pushes per-arch manifests to local-ai-backends with # push-by-digest=true (no tag), then anchors a tagged copy into # ci-cache so the manifest can be retrieved hours later when this # merge runs. Quay's manifest GC, however, is per-repository: the # anchor tag in ci-cache protects the manifest there, but the same # digest in local-ai-backends has no tag in *that* repo and gets # reaped independently. Sourcing local-ai-backends@ here # then fails with "manifest not found" — exactly the regression # we hit on v4.2.2 (19/37 multiarch merges failed). # # ci-cache@ resolves because we anchored it there. buildx # imagetools create copies the manifest into local-ai-backends # (cross-repo within the same registry, blobs already cross-mounted # from the original push so no transfer needed) and publishes the # manifest list with the user-facing tags. The resulting manifest # list is fully self-contained in local-ai-backends — child digests # only, no embedded references to ci-cache. - name: Create manifest list and push (quay) if: github.event_name != 'pull_request' working-directory: /tmp/digests run: | set -euo pipefail tags=$(jq -cr ' .tags | map(select(startswith("quay.io/"))) | map("-t " + .) | join(" ") ' <<< "$DOCKER_METADATA_OUTPUT_JSON") if [ -z "$tags" ]; then echo "No quay.io tags from docker/metadata-action; skipping quay merge" else # shellcheck disable=SC2086 docker buildx imagetools create $tags \ $(printf 'quay.io/go-skynet/ci-cache@sha256:%s ' *) fi - name: Create manifest list and push (dockerhub) if: github.event_name != 'pull_request' working-directory: /tmp/digests run: | set -euo pipefail tags=$(jq -cr ' .tags | map(select(startswith("localai/"))) | map("-t " + .) | join(" ") ' <<< "$DOCKER_METADATA_OUTPUT_JSON") if [ -z "$tags" ]; then echo "No dockerhub tags from docker/metadata-action; skipping dockerhub merge" else # shellcheck disable=SC2086 docker buildx imagetools create $tags \ $(printf 'localai/localai-backends@sha256:%s ' *) fi - name: Inspect manifest if: github.event_name != 'pull_request' run: | set -euo pipefail first_tag=$(jq -cr '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON") if [ -n "$first_tag" ] && [ "$first_tag" != "null" ]; then docker buildx imagetools inspect "$first_tag" fi # See .github/scripts/cleanup-keepalive-tags.sh for why this is # best-effort and what the failure modes are. - name: Cleanup keepalive tags in ci-cache if: github.event_name != 'pull_request' && success() env: TAG_SUFFIX: ${{ inputs.tag-suffix }} QUAY_TOKEN: ${{ secrets.quayPassword }} run: .github/scripts/cleanup-keepalive-tags.sh - name: Job summary if: github.event_name != 'pull_request' run: | set -euo pipefail echo "Merged manifest tags:" >> "$GITHUB_STEP_SUMMARY" jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON" | sed 's/^/- /' >> "$GITHUB_STEP_SUMMARY" echo >> "$GITHUB_STEP_SUMMARY" echo "Per-arch digests:" >> "$GITHUB_STEP_SUMMARY" ls -1 /tmp/digests | sed 's/^/- sha256:/' >> "$GITHUB_STEP_SUMMARY"