From ef9cca93c7b22a5286cf62df04f8ab7fbb58f004 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 28 Oct 2026 07:13:30 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20fix:=20Build=20the=20arm64=20Ima?= =?UTF-8?q?ge=20on=20a=20Native=20Runner=20So=20Publishing=20Stops=20Stall?= =?UTF-8?q?ing=20Under=20QEMU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish workflow built linux/amd64 and linux/arm64 in one job on an amd64 runner, emulating arm64 with QEMU. bun install stalls there: one run sat in "Build and push" for over two hours, and the next one's arm64 `bun install --frozen-lockfile` went silent for 13 minutes (848.7s, against 26.5s the run before, on the same BuildKit and binfmt images). Each architecture now builds on a runner of that architecture (ubuntu-24.04-arm for arm64, free for public repositories) and pushes an untagged digest; a merge job then joins the two digests into the tagged multi-platform image with `docker buildx imagetools create`, as LibreChat's own publish workflow does. The merge refuses to publish unless both architectures built (exactly two digests), and a failing leg no longer cancels the other one, so "Re-run failed jobs" has the healthy leg's digest to reuse (kept for 7 days). `latest` is now decided in the merge job, right before it is pushed. The check is the same (this commit is on main and no later commit changed the build context), but every re-run of the merge repeats it: GitHub reuses a successful job's outputs when failed jobs are re-run, so a decision taken at the start of the run could move `latest` back after newer images shipped. Releases no longer tag `latest` either: metadata-action's default `flavor: latest=auto` added it to every semver release, skipping the check; `latest=false` leaves the merge as its only source. A publishing run that computes no tags now fails instead of pushing nothing (a release tag that isn't semver, say), and a manual run on a branch other than main builds both architectures without pushing, as a dry run. The image's `org.opencontainers.image.version` label is now the commit sha on main (it was `latest`, a tag that moves) and the version on releases, as before. --- .github/workflows/docker-publish.yml | 157 +++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f3916fd1..dcc40c36 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -17,33 +17,118 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} queue: max +permissions: + contents: read + +env: + IMAGE: ghcr.io/librechat-ai/librechat-admin-panel + jobs: - build: + prepare: + name: Prepare tags runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + publish: ${{ steps.meta.outputs.tags != '' }} + + steps: + - name: Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE }} + flavor: latest=false + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,format=long,prefix=,enable={{is_default_branch}} + + - name: Refuse a publishing run without tags + if: github.event_name != 'workflow_dispatch' && steps.meta.outputs.tags == '' + run: | + echo "::error::No image tags for $GITHUB_REF: release tags must be semver (vX.Y.Z)." + exit 1 + + build: + name: Build (${{ matrix.arch }}) + needs: prepare + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} timeout-minutes: 30 permissions: contents: read packages: write + strategy: + fail-fast: false + matrix: + arch: [amd64, arm64] steps: - name: Checkout uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry + if: needs.prepare.outputs.publish == 'true' uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push by digest + id: build + uses: docker/build-push-action@v7 + with: + context: . + file: Dockerfile + platforms: linux/${{ matrix.arch }} + labels: ${{ needs.prepare.outputs.labels }} + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=${{ needs.prepare.outputs.publish }} + + - name: Export digest + if: needs.prepare.outputs.publish == 'true' + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + if [ -z "$DIGEST" ]; then + echo "The build produced no digest." >&2 + exit 1 + fi + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + + - name: Upload digest + if: needs.prepare.outputs.publish == 'true' + uses: actions/upload-artifact@v7 + with: + name: digests-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 7 + overwrite: true + + merge: + name: Merge and tag + needs: [prepare, build] + if: needs.prepare.outputs.publish == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + - name: Check whether main's head still builds this image id: head run: | @@ -56,24 +141,48 @@ jobs: fi echo "latest=$latest" >> "$GITHUB_OUTPUT" - - name: Docker metadata - id: meta - uses: docker/metadata-action@v6 + - name: Download digests + uses: actions/download-artifact@v8 with: - images: ghcr.io/librechat-ai/librechat-admin-panel - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest,enable=${{ steps.head.outputs.latest }} - type=sha,format=long,prefix=,enable={{is_default_branch}} + pattern: digests-* + merge-multiple: true + path: ${{ runner.temp }}/digests - - name: Build and push - uses: docker/build-push-action@v7 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 with: - context: . - file: Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create the manifest list and push its tags + env: + TAGS: ${{ needs.prepare.outputs.tags }} + LATEST: ${{ steps.head.outputs.latest }} + run: | + shopt -s nullglob + mkdir -p "$RUNNER_TEMP/digests" + cd "$RUNNER_TEMP/digests" + digests=(*) + if [ "${#digests[@]}" -ne 2 ]; then + echo "Expected 2 platform digests (amd64, arm64), found ${#digests[@]}: ${digests[*]}. If the digest artifacts expired, use Re-run all jobs." >&2 + exit 1 + fi + tag_args=() + while IFS= read -r tag; do + if [ -n "$tag" ]; then tag_args+=(-t "$tag"); fi + done <<< "$TAGS" + if [ "${#tag_args[@]}" -eq 0 ]; then + echo "No tags to publish; refusing to push an untagged manifest list." >&2 + exit 1 + fi + if [ "$LATEST" = true ]; then tag_args+=(-t "$IMAGE:latest"); fi + docker buildx imagetools create "${tag_args[@]}" "${digests[@]/#/$IMAGE@sha256:}" + + - name: Inspect the image + env: + TAGS: ${{ needs.prepare.outputs.tags }} + run: docker buildx imagetools inspect "$(head -n 1 <<< "$TAGS")"