From 85f6be748ca8723af0a3a0c4c935e3649751171e Mon Sep 17 00:00:00 2001 From: Jon Palmer Date: Sun, 24 May 2026 13:58:09 -0700 Subject: [PATCH 1/5] fix(docker): force pytantan to build without any SIMD backends The previous CMAKE_ARGS env-var approach (PR #64) did not reliably disable AVX2 in the shipped image, leaving Rosetta 2 / non-AVX2 x86_64 hosts to crash with SIGILL at pytantan import time. Switch to passing HAVE_AVX2/HAVE_SSE4/HAVE_NEON=OFF (plus empty *_C_FLAGS) directly through pip via --config-settings, which is the scikit-build-core supported channel and is not subject to env-var filtering. Also add a build-time guard that fails the Docker build if any avx2/sse4/neon platform module remains or if any pytantan .so contains AVX/AVX2 instructions (ymm/zmm/vpbroadcast/vextracti128/ vinserti128) as detected by objdump, plus a Python import smoke test. Refs: althonos/pytantan#2 --- Dockerfile | 60 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index f3f7a89..c003f61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,21 +21,65 @@ COPY funannotate2 ./funannotate2 # Install from the lockfile; no-op if the lockfile already matches. RUN pixi install --locked -# Rebuild pytantan from source with AVX2 disabled (SSE4-only baseline). -# The PyPI/bioconda wheel ships with AVX2 SIMD which SIGILLs on CPUs that lack -# it — notably Rosetta 2 on Apple Silicon — and AVX2 gives no meaningful win -# for this pipeline. `pip` uses build isolation, so scikit-build-core/cython/ -# scoring-matrices are pulled in transparently; we only need the C/C++ toolchain. +# Rebuild pytantan from source with all SIMD backends disabled. +# +# Background: pytantan's wheels ship with AVX2 enabled, and prior to v0.1.4 +# the generic path was also polluted with AVX2 flags via `add_compile_options` +# in the project-level CMakeLists.txt. Either way, on x86_64 CPUs without AVX2 +# (notably Rosetta 2 on Apple Silicon) any code path that executes an AVX2 +# instruction raises SIGILL. The runtime-dispatch added in v0.1.4 does not +# help us here because we want a build that is portable to *any* x86_64. +# +# pytantan's CMakeLists.txt uses FindAVX2/FindSSE4/FindNEON which auto-detect +# on the build host (GitHub Actions runners have AVX2), so we have to force +# the HAVE_* flags OFF. Pre-defining them in FindAVX2.cmake's `if((DEFINED ...))` +# guard short-circuits detection entirely. +# +# We pass these via pip's `--config-settings=cmake.define.*` rather than +# CMAKE_ARGS/SKBUILD_CMAKE_ARGS env vars, since that route goes directly into +# scikit-build-core's parser and is not subject to env-var filtering. ARG PYTANTAN_VERSION RUN apt-get update && \ apt-get install -y --no-install-recommends \ - git build-essential cmake zlib1g-dev ca-certificates && \ + git build-essential cmake zlib1g-dev binutils ca-certificates && \ rm -rf /var/lib/apt/lists/* -RUN CMAKE_ARGS="-DHAVE_AVX2=OFF -DAVX2_C_FLAGS= -DHAVE_SSE4=OFF -DSSE4_C_FLAGS= -DHAVE_NEON=OFF -DNEON_C_FLAGS=" \ - /app/.pixi/envs/default/bin/pip install --no-deps --force-reinstall \ +RUN /app/.pixi/envs/default/bin/pip install \ + --no-deps --no-cache-dir --force-reinstall -v \ + --config-settings=cmake.define.HAVE_AVX2=OFF \ + --config-settings=cmake.define.HAVE_SSE4=OFF \ + --config-settings=cmake.define.HAVE_NEON=OFF \ + --config-settings=cmake.define.AVX2_C_FLAGS= \ + --config-settings=cmake.define.SSE4_C_FLAGS= \ + --config-settings=cmake.define.NEON_C_FLAGS= \ "pytantan @ git+https://github.com/althonos/pytantan.git@v${PYTANTAN_VERSION}" && \ rm -rf /root/.cache/pip +# Verify the rebuild was effective: only `generic` should be present in the +# platform/ directory and neither lib.*.so nor generic.*.so should contain +# any AVX/AVX2 instructions (no `ymm`/`zmm` register references, no `vex`- +# encoded vector ops). Fail the build loudly otherwise so we never ship an +# image that SIGILLs at import time. +RUN set -eux; \ + PY=/app/.pixi/envs/default/bin/python; \ + PT_DIR=$("$PY" -c 'import pytantan, os; print(os.path.dirname(pytantan.__file__))'); \ + echo "pytantan installed at: $PT_DIR"; \ + ls -la "$PT_DIR/platform/"; \ + SIMD_MODS=$(ls "$PT_DIR/platform/" | grep -E '^(avx2|sse4|neon)\.' || true); \ + if [ -n "$SIMD_MODS" ]; then \ + echo "ERROR: SIMD platform modules were built despite HAVE_*=OFF: $SIMD_MODS"; \ + exit 1; \ + fi; \ + BAD=""; \ + for so in $(find "$PT_DIR" -maxdepth 2 -name '*.so'); do \ + if objdump -d -M intel --no-show-raw-insn "$so" 2>/dev/null \ + | grep -Eq '\b(ymm[0-9]+|zmm[0-9]+|vpbroadcast|vextracti128|vinserti128)\b'; then \ + echo "ERROR: $so contains AVX/AVX2 instructions"; \ + BAD="${BAD} ${so}"; \ + fi; \ + done; \ + if [ -n "$BAD" ]; then exit 1; fi; \ + "$PY" -c "import pytantan; from pytantan import Alphabet, RepeatFinder, default_scoring_matrix; print('pytantan smoke test OK', pytantan.__version__)" + # Pre-generate an activation script so the final image doesn't need pixi. RUN mkdir -p /app/bin && \ { echo '#!/bin/bash'; \ From a4138fc33d754866a98dddb97cfbdceda605bbb5 Mon Sep 17 00:00:00 2001 From: Jon Palmer Date: Sun, 24 May 2026 14:10:36 -0700 Subject: [PATCH 2/5] ci(docker): smoke-test image under non-AVX2 CPU emulation before push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a step that runs the built image with qemu-x86_64-static and a Westmere CPU model (no AVX, no AVX2), then imports pytantan inside it. Any AVX/AVX2 instruction triggered at import time raises SIGILL under QEMU TCG and fails the workflow — the same behavior users see on Rosetta 2 on Apple Silicon and on older x86_64 hardware. Restructured the build step so the image is first loaded to the local Docker daemon, smoke-tested, and only pushed to GHCR / Docker Hub if the smoke test passes. The push step is a cache-from rebuild that does not re-execute any layers. --- .github/workflows/docker.yml | 55 +++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e1a5ae5..0fed44f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -81,21 +81,68 @@ jobs: type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - - name: Build and push + # Build the image into the local Docker daemon first (no registry push + # yet) so we can smoke-test it before publishing. The cache-to step makes + # the subsequent push-from-cache step essentially a no-op rebuild. + - name: Build image (load to local Docker daemon) id: build uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} + push: false + load: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: false + # Smoke-test the built image against a CPU model that lacks AVX2, to + # reproduce the Rosetta 2 / non-AVX2 x86_64 environment some users run on. + # We use qemu-user-static with `-cpu Westmere` (pre-AVX); any AVX/AVX2 + # instruction executed by pytantan will raise SIGILL and fail the job. + - name: Install qemu-user-static + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends qemu-user-static + + - name: Resolve test image tag + id: testimg + run: | + tag=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + + - name: Smoke test pytantan under non-AVX2 CPU emulation (Westmere) + run: | + set -euxo pipefail + docker run --rm \ + -v /usr/bin/qemu-x86_64-static:/usr/bin/qemu-x86_64-static:ro \ + --entrypoint /usr/bin/qemu-x86_64-static \ + "${{ steps.testimg.outputs.tag }}" \ + -cpu Westmere \ + /app/.pixi/envs/default/bin/python -c \ + "import pytantan; from pytantan import Alphabet, RepeatFinder, default_scoring_matrix; print('pytantan smoke OK', pytantan.__version__)" + + # Only publish to the registries after the smoke test has passed. + # The second build is a cache hit on every layer; it just exports the + # already-built image to the registry. + - name: Push image to registries + if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + provenance: false + - name: Image digest - if: steps.build.outputs.digest != '' - run: echo "Digest=${{ steps.build.outputs.digest }}" + if: steps.push.outputs.digest != '' + run: echo "Digest=${{ steps.push.outputs.digest }}" From 1673b45fea6929401eff36982e42357973e5250b Mon Sep 17 00:00:00 2001 From: Jon Palmer Date: Sun, 24 May 2026 15:41:47 -0700 Subject: [PATCH 3/5] ci(docker): smoke-test image on Apple Silicon (macos-14 + Rosetta 2) Replace the slow QEMU-Westmere smoke test (which took >40 minutes in TCG emulation) with a real Apple Silicon (M1) runner that pulls the built image and executes it under Rosetta 2 -- the exact environment that triggers the pytantan SIGILL on user machines. The workflow is now three jobs: build ubuntu-latest. Builds the image and pushes a transient "staging-" tag to GHCR. smoke-test-rosetta macos-14. Starts colima with --vm-type=vz --vz-rosetta, pulls the staging image with --platform=linux/amd64, and runs 'python -c "import pytantan; ..."' under Rosetta. If pytantan was built with AVX/AVX2, this fails with SIGILL. promote ubuntu-latest. Only runs if the smoke test passes and the event is not a PR / inputs.push is true. Uses 'docker buildx imagetools create' to copy the staging tag manifest to all release tags (latest, vX.Y, sha-...), no rebuild or repull. Fork-PRs (no GHCR push permission) skip the staging push and the smoke test; the in-Dockerfile objdump/file-presence checks still run as part of the build itself. --- .github/workflows/docker.yml | 151 +++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 50 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0fed44f..89f2bff 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -30,6 +30,11 @@ jobs: name: Build funannotate2 image (linux/amd64) runs-on: ubuntu-latest timeout-minutes: 240 + outputs: + staging_tag: ${{ steps.staging.outputs.tag }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + dockerhub_enabled: ${{ env.DOCKERHUB_ENABLED }} env: DOCKERHUB_ENABLED: ${{ secrets.DOCKERHUB_TOKEN != '' && 'true' || 'false' }} @@ -52,20 +57,13 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to GHCR - if: github.event_name != 'pull_request' + if: ${{ github.event.pull_request.head.repo.fork != true }} uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Log in to Docker Hub - if: ${{ github.event_name != 'pull_request' && env.DOCKERHUB_ENABLED == 'true' }} - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract image metadata id: meta uses: docker/metadata-action@v5 @@ -81,68 +79,121 @@ jobs: type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - # Build the image into the local Docker daemon first (no registry push - # yet) so we can smoke-test it before publishing. The cache-to step makes - # the subsequent push-from-cache step essentially a no-op rebuild. - - name: Build image (load to local Docker daemon) + # Compute a single transient "staging" tag in GHCR. The smoke-test job + # pulls this tag on a real Apple Silicon runner under Rosetta 2; the + # image is only copied to the release tags (latest, vX.Y, sha, ...) once + # the smoke test passes. + - name: Compute staging tag + id: staging + run: | + echo "tag=ghcr.io/nextgenusfs/funannotate2:staging-${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Build image and push staging tag to GHCR id: build uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile platforms: linux/amd64 - push: false - load: true - tags: ${{ steps.meta.outputs.tags }} + push: ${{ github.event.pull_request.head.repo.fork != true }} + tags: ${{ steps.staging.outputs.tag }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: false - # Smoke-test the built image against a CPU model that lacks AVX2, to - # reproduce the Rosetta 2 / non-AVX2 x86_64 environment some users run on. - # We use qemu-user-static with `-cpu Westmere` (pre-AVX); any AVX/AVX2 - # instruction executed by pytantan will raise SIGILL and fail the job. - - name: Install qemu-user-static + - name: Image digest + if: steps.build.outputs.digest != '' + run: echo "Digest=${{ steps.build.outputs.digest }}" + + # Smoke-test the staged image on a real Apple Silicon (M1) macOS runner + # under Rosetta 2 -- exactly the environment where pytantan SIGILLs when + # built with AVX2. If `import pytantan` succeeds here the binary is safe + # to publish to the release tags. + smoke-test-rosetta: + name: Verify pytantan under Apple Rosetta 2 (macos-14) + needs: build + if: ${{ github.event.pull_request.head.repo.fork != true }} + runs-on: macos-14 + timeout-minutes: 60 + permissions: + contents: read + packages: read + + steps: + - name: Install colima and docker CLI + run: | + brew install colima docker + + - name: Start colima with VZ + Rosetta 2 (amd64 acceleration) + run: | + colima start \ + --arch aarch64 \ + --vm-type=vz \ + --vz-rosetta \ + --cpu 3 \ + --memory 6 \ + --disk 30 + docker info + docker version + + - name: Log in to GHCR run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends qemu-user-static + echo "${{ secrets.GITHUB_TOKEN }}" \ + | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - name: Resolve test image tag - id: testimg + - name: Pull staging image (linux/amd64) run: | - tag=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) - echo "tag=${tag}" >> "$GITHUB_OUTPUT" + docker pull --platform=linux/amd64 "${{ needs.build.outputs.staging_tag }}" + docker image inspect "${{ needs.build.outputs.staging_tag }}" \ + --format '{{.Architecture}}/{{.Os}}' - - name: Smoke test pytantan under non-AVX2 CPU emulation (Westmere) + - name: Smoke test pytantan import under Rosetta 2 run: | set -euxo pipefail - docker run --rm \ - -v /usr/bin/qemu-x86_64-static:/usr/bin/qemu-x86_64-static:ro \ - --entrypoint /usr/bin/qemu-x86_64-static \ - "${{ steps.testimg.outputs.tag }}" \ - -cpu Westmere \ + docker run --rm --platform=linux/amd64 \ + "${{ needs.build.outputs.staging_tag }}" \ /app/.pixi/envs/default/bin/python -c \ "import pytantan; from pytantan import Alphabet, RepeatFinder, default_scoring_matrix; print('pytantan smoke OK', pytantan.__version__)" - # Only publish to the registries after the smoke test has passed. - # The second build is a cache hit on every layer; it just exports the - # already-built image to the registry. - - name: Push image to registries - if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} - id: push - uses: docker/build-push-action@v6 + # Promote the staging tag to the real release tags. `docker buildx + # imagetools create` performs a registry-side copy of the manifest, so + # this finishes in seconds and never re-pulls the image bytes. + promote: + name: Promote staging image to release tags + needs: [build, smoke-test-rosetta] + if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} + runs-on: ubuntu-latest + env: + DOCKERHUB_ENABLED: ${{ needs.build.outputs.dockerhub_enabled }} + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 with: - context: . - file: ./Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - provenance: false + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Image digest - if: steps.push.outputs.digest != '' - run: echo "Digest=${{ steps.push.outputs.digest }}" + - name: Log in to Docker Hub + if: ${{ env.DOCKERHUB_ENABLED == 'true' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Copy staging tag to release tags + env: + STAGING_TAG: ${{ needs.build.outputs.staging_tag }} + TAGS: ${{ needs.build.outputs.tags }} + run: | + set -euxo pipefail + printf '%s\n' "${TAGS}" | while IFS= read -r tag; do + [ -z "${tag}" ] && continue + echo "Promoting ${STAGING_TAG} -> ${tag}" + docker buildx imagetools create --tag "${tag}" "${STAGING_TAG}" + done From 2e5c865da11d93fc312446e3759ca0f597827339 Mon Sep 17 00:00:00 2001 From: Jon Palmer Date: Sun, 24 May 2026 18:47:12 -0700 Subject: [PATCH 4/5] ci(docker): install Rosetta 2 before starting colima --vz-rosetta GitHub macos-14 runners ship without Rosetta 2 by default; without it 'colima start --vm-type=vz --vz-rosetta' aborts at 'Setting up Rosetta share' with a non-zero exit. Install Rosetta via softwareupdate before the colima start step. Also add an on-failure step that dumps colima's host-agent logs so future failures can be diagnosed without rerunning. Refs: https://github.com/nextgenusfs/funannotate2/actions/runs/26374795029/job/77634321942 --- .github/workflows/docker.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 89f2bff..bbe42b7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -121,6 +121,13 @@ jobs: packages: read steps: + - name: Install Rosetta 2 + # macos-14 (Apple Silicon) GitHub runners do not ship with Rosetta + # pre-installed; `colima --vz-rosetta` needs it to set up the in-VM + # binfmt translator for linux/amd64 containers. + run: | + sudo softwareupdate --install-rosetta --agree-to-license + - name: Install colima and docker CLI run: | brew install colima docker @@ -137,6 +144,19 @@ jobs: docker info docker version + - name: Dump colima logs on failure + if: failure() + run: | + echo "=== ha.stderr.log ===" + cat /Users/runner/.colima/_lima/colima/ha.stderr.log || true + echo "=== ha.stdout.log ===" + cat /Users/runner/.colima/_lima/colima/ha.stdout.log || true + echo "=== serial*.log ===" + for f in /Users/runner/.colima/_lima/colima/serial*.log; do + echo "--- $f ---" + cat "$f" || true + done + - name: Log in to GHCR run: | echo "${{ secrets.GITHUB_TOKEN }}" \ From dd28fc06b02c3abae506922208b01b63a596ea44 Mon Sep 17 00:00:00 2001 From: Jon Palmer Date: Sun, 24 May 2026 20:26:19 -0700 Subject: [PATCH 5/5] ci(docker): drop runtime smoke test, rely on Dockerfile static checks GitHub-hosted macos-14 runners are themselves VMs and do not expose Apple's Virtualization Framework to guests (no nested virtualization), so colima --vm-type=vz --vz-rosetta aborts with VZErrorDomain Code=2 'Virtualization is not available on this hardware'. The earlier QEMU TCG smoke test on ubuntu-latest, while functionally correct, takes 15-25 min to import pytantan through the pixi env. Neither option is suitable for CI. Revert the workflow to a single build-and-push job and rely on the Dockerfile's build-time guards (file-presence check for pytantan/platform/*.so SIMD variants and objdump scan of every shipped .so for AVX2 mnemonics) to ensure no AVX2-tainted binary is ever published. Refs: https://github.com/actions/runner-images/issues/9460 --- .github/workflows/docker.yml | 140 +++-------------------------------- 1 file changed, 11 insertions(+), 129 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bbe42b7..e1a5ae5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -30,11 +30,6 @@ jobs: name: Build funannotate2 image (linux/amd64) runs-on: ubuntu-latest timeout-minutes: 240 - outputs: - staging_tag: ${{ steps.staging.outputs.tag }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - dockerhub_enabled: ${{ env.DOCKERHUB_ENABLED }} env: DOCKERHUB_ENABLED: ${{ secrets.DOCKERHUB_TOKEN != '' && 'true' || 'false' }} @@ -57,13 +52,20 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to GHCR - if: ${{ github.event.pull_request.head.repo.fork != true }} + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to Docker Hub + if: ${{ github.event_name != 'pull_request' && env.DOCKERHUB_ENABLED == 'true' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract image metadata id: meta uses: docker/metadata-action@v5 @@ -79,24 +81,15 @@ jobs: type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - # Compute a single transient "staging" tag in GHCR. The smoke-test job - # pulls this tag on a real Apple Silicon runner under Rosetta 2; the - # image is only copied to the release tags (latest, vX.Y, sha, ...) once - # the smoke test passes. - - name: Compute staging tag - id: staging - run: | - echo "tag=ghcr.io/nextgenusfs/funannotate2:staging-${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - - - name: Build image and push staging tag to GHCR + - name: Build and push id: build uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile platforms: linux/amd64 - push: ${{ github.event.pull_request.head.repo.fork != true }} - tags: ${{ steps.staging.outputs.tag }} + push: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max @@ -106,114 +99,3 @@ jobs: if: steps.build.outputs.digest != '' run: echo "Digest=${{ steps.build.outputs.digest }}" - # Smoke-test the staged image on a real Apple Silicon (M1) macOS runner - # under Rosetta 2 -- exactly the environment where pytantan SIGILLs when - # built with AVX2. If `import pytantan` succeeds here the binary is safe - # to publish to the release tags. - smoke-test-rosetta: - name: Verify pytantan under Apple Rosetta 2 (macos-14) - needs: build - if: ${{ github.event.pull_request.head.repo.fork != true }} - runs-on: macos-14 - timeout-minutes: 60 - permissions: - contents: read - packages: read - - steps: - - name: Install Rosetta 2 - # macos-14 (Apple Silicon) GitHub runners do not ship with Rosetta - # pre-installed; `colima --vz-rosetta` needs it to set up the in-VM - # binfmt translator for linux/amd64 containers. - run: | - sudo softwareupdate --install-rosetta --agree-to-license - - - name: Install colima and docker CLI - run: | - brew install colima docker - - - name: Start colima with VZ + Rosetta 2 (amd64 acceleration) - run: | - colima start \ - --arch aarch64 \ - --vm-type=vz \ - --vz-rosetta \ - --cpu 3 \ - --memory 6 \ - --disk 30 - docker info - docker version - - - name: Dump colima logs on failure - if: failure() - run: | - echo "=== ha.stderr.log ===" - cat /Users/runner/.colima/_lima/colima/ha.stderr.log || true - echo "=== ha.stdout.log ===" - cat /Users/runner/.colima/_lima/colima/ha.stdout.log || true - echo "=== serial*.log ===" - for f in /Users/runner/.colima/_lima/colima/serial*.log; do - echo "--- $f ---" - cat "$f" || true - done - - - name: Log in to GHCR - run: | - echo "${{ secrets.GITHUB_TOKEN }}" \ - | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - - name: Pull staging image (linux/amd64) - run: | - docker pull --platform=linux/amd64 "${{ needs.build.outputs.staging_tag }}" - docker image inspect "${{ needs.build.outputs.staging_tag }}" \ - --format '{{.Architecture}}/{{.Os}}' - - - name: Smoke test pytantan import under Rosetta 2 - run: | - set -euxo pipefail - docker run --rm --platform=linux/amd64 \ - "${{ needs.build.outputs.staging_tag }}" \ - /app/.pixi/envs/default/bin/python -c \ - "import pytantan; from pytantan import Alphabet, RepeatFinder, default_scoring_matrix; print('pytantan smoke OK', pytantan.__version__)" - - # Promote the staging tag to the real release tags. `docker buildx - # imagetools create` performs a registry-side copy of the manifest, so - # this finishes in seconds and never re-pulls the image bytes. - promote: - name: Promote staging image to release tags - needs: [build, smoke-test-rosetta] - if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.push) }} - runs-on: ubuntu-latest - env: - DOCKERHUB_ENABLED: ${{ needs.build.outputs.dockerhub_enabled }} - - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - if: ${{ env.DOCKERHUB_ENABLED == 'true' }} - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Copy staging tag to release tags - env: - STAGING_TAG: ${{ needs.build.outputs.staging_tag }} - TAGS: ${{ needs.build.outputs.tags }} - run: | - set -euxo pipefail - printf '%s\n' "${TAGS}" | while IFS= read -r tag; do - [ -z "${tag}" ] && continue - echo "Promoting ${STAGING_TAG} -> ${tag}" - docker buildx imagetools create --tag "${tag}" "${STAGING_TAG}" - done -