Add multi-arch (amd64/arm64) Docker builds with manifest stitching#72
Add multi-arch (amd64/arm64) Docker builds with manifest stitching#72t0mdavid-m wants to merge 18 commits into
Conversation
Mirror FLASHApp's split-build / manifest-merge approach so both linux/amd64 and linux/arm64 are published for the full and simple variants. Existing `<ref>-full` / `<ref>-simple` / `latest` tags become multi-arch manifests — k8s overlays, docker-compose users, and direct `docker pull` callers transparently get the right arch. Dockerfile.arm (delta from Dockerfile): - aarch64 miniforge installer - conditional THIRDPARTY/Linux/aarch64 copy (some OpenMS releases ship an empty/missing aarch64 dir) - pruned thirdparty PATH to tools that actually have ARM builds: LuciPHOr2, MSGFPlus, ThermoRawFileParser, Comet, Percolator, Sage Dockerfile_simple.arm (delta from Dockerfile_simple): - aarch64 miniforge installer only — pyOpenMS ships aarch64 wheels on PyPI, so `pip install -r requirements.txt` works as-is The shared docker/entrypoint.sh is reused as-is on ARM: its apptainer/read-only-root handling is arch-neutral and worth keeping. Base stays ubuntu:22.04 (Redis 6.0 predates the ARM64-COW-BUG warning, so no `--ignore-warnings` flag needed). Workflow changes (build-and-test.yml): - `build` renamed `build-amd64`; per-arch tags carry `-amd64`. - New `build-arm64` job runs on `ubuntu-24.04-arm`, builds the `.arm` Dockerfiles for both variants, ends with a pull-back + /_stcore/health probe on push events. - New `create-manifest` job stitches `<ref>-<variant>-amd64` + `<ref>-<variant>-arm64` into multi-arch `<ref>-<variant>` and `latest` manifests. - test-apptainer / test-nginx / test-traefik / publish-apptainer keep consuming the amd64 artifact only. SIF publishing stays amd64-only this iteration. - PRs build both arches (registry cache keeps warm runs cheap) but don't push; manifest creation also skipped on PRs. Branch-protection note: the `build` required check is renamed to `build-amd64`. Admins should update protected-branch rules and add `build-arm64` / `create-manifest` if those should also be required.
Previously the apptainer/nginx/traefik integration tests only ran
against the amd64 artifact, so the arm64 image was validated solely
by its build succeeding plus a post-push /_stcore/health probe. Now
all three integration matrices fan out over arch=[amd64, arm64] with
a matrix-driven runs-on, exercising the read-only-root apptainer
contract and both kind-based ingress paths on a native ARM runner
too.
Changes:
- `build-amd64` artifact renamed from
`openms-streamlit-<variant>-image` to
`openms-streamlit-<variant>-amd64-image` for symmetry.
- `build-arm64` now also `load: true`'s the built image, retags to
the kind-friendly `openms-streamlit:test`, saves it as a tar, and
uploads it as `openms-streamlit-<variant>-arm64-image`. The
post-push pull-back smoke test is removed — the new apptainer/
nginx/traefik runs subsume it and avoid the slow GHCR pull.
- `test-apptainer`, `test-nginx`, `test-traefik` matrices switched
from `variant: [full, simple]` to an `include:` list with
{variant, arch, runner} tuples; `runs-on: ${{ matrix.runner }}`
selects `ubuntu-latest` for amd64 and `ubuntu-24.04-arm` for arm64.
Artifact download names get `${{ matrix.arch }}` interpolated.
- SIF upload at the tail of `test-apptainer` gated on
`matrix.arch == 'amd64'`: arm64 still runs the full apptainer
contract end-to-end, but only amd64 produces the SIF that
`publish-apptainer` ships to GHCR (HPC SIF consumers are amd64).
Note on `publish-apptainer`: it stays on `needs: test-apptainer`,
which now waits for the arm64 matrix entries too — meaning an arm64
apptainer regression will block amd64 SIF publishing. Conservative
on purpose; happy to decouple via separate jobs if that turns out to
be too strict in practice.
The ARM build of `make -j4 TOPP` failed at the link step with
/usr/bin/ld: /root/miniforge3/lib/libyaml-cpp.so.0.8: undefined
reference to `std::ios_base_library_init()@GLIBCXX_3.4.32'
The conda-forge libyaml-cpp wheel for aarch64 is built against
GLIBCXX_3.4.32 (gcc 13+), but Ubuntu 22.04's system g++ ships with an
older libstdc++. Running cmake inside the mamba shell lets it discover
/root/miniforge3/lib first, so the conda-forge yaml-cpp gets linked
into every TOPP binary and breaks. amd64 happens to work because the
conda-forge amd64 yaml-cpp build is older.
Fix mirrors FLASHApp's Dockerfile.arm: configure OpenMS in two cmake
passes — pass 1 under plain `/bin/bash` with
`-DCMAKE_IGNORE_PREFIX_PATH=/root/miniforge3` so cmake resolves C++
deps from the system tree (libyaml-cpp from contrib, boost from apt,
etc.); pass 2 under `mamba run` with `-DPYOPENMS=ON` so the Python
bindings still find conda-forge Python / Cython / NumPy. The
IGNORE_PREFIX_PATH flag is repeated on pass 2 to keep the cached C++
link command unchanged.
Only Dockerfile.arm changes; Dockerfile (amd64) keeps its single-pass
cmake to avoid disturbing the working x86 path.
The two-pass cmake split from 1d73b67 runs pass 1 under `SHELL ["/bin/bash", "-c"]`, but the only cmake on the image is the one from `mamba install cmake` at /root/miniforge3/envs/streamlit-env/bin/cmake — not on plain bash's PATH. Result: exit 127 (command not found) the moment pass 1 invokes cmake. FLASHApp.arm sidesteps this by installing cmake via apt; do the same here (just append `cmake` to the existing apt-get install line). The mamba cmake install stays, so pass 2 under the mamba shell continues to use the conda-forge cmake exactly as it did before. Ubuntu 22.04 ships cmake 3.22, comfortably above OpenMS 3.5's 3.15 floor.
The previous fix (install cmake via apt) didn't actually help: OpenMS
3.5's CMakeLists.txt requires cmake >= 3.24, and Ubuntu 22.04's apt
cmake is 3.22.1, which fails configure with
CMake Error at src/openms/extern/CMakeLists.txt:11 (cmake_minimum_required):
CMake 3.24 or higher is required. You are running version 3.22.1
That's exactly why the existing x86 Dockerfile installs cmake via
mamba (the conda-forge build is 3.30+). FLASHApp.arm escapes this by
using ubuntu:24.04 (apt cmake 3.28); we stay on 22.04 to minimize
churn vs. the working x86 Dockerfile.
Fix: in pass 1, call the mamba-env cmake by its full path
`/root/miniforge3/envs/streamlit-env/bin/cmake`. The plain-bash SHELL
is still in effect, so cmake doesn't pick up any conda-forge
environment side effects, and CMAKE_IGNORE_PREFIX_PATH keeps it from
auto-discovering miniforge libraries during find_package. The cmake
binary itself runs against miniforge's libstdc++, but that's a
runtime detail of cmake — it doesn't leak into the configured
project's link command.
The apt cmake addition from f11bc99 is now redundant but harmless;
leaving it in place to keep this diff focused.
Two failures in the previous run (test-traefik full, test-nginx simple) ended with the runner reporting "No space left on device" while flushing its diagnostic log. ubuntu-latest starts with ~14 GB free; downloading the full image artifact (5-8 GB), loading it into docker (decompressed, larger), pulling kind's node image, then loading the OCI tar into the kind cluster easily exceeds that budget. Mirror the cleanup already used by `build-arm64`: drop the runner's preinstalled dotnet / android SDK / ghc / hostedtoolcache to recover ~30 GB. Same step now runs at the top of test-apptainer, test-nginx, and test-traefik on both amd64 (ubuntu-latest) and arm64 (ubuntu-24.04-arm) matrix entries — the arm runner is at least as tight as amd64.
After the two-pass cmake configure landed in 5185c3e, the next attempt got past `make -j4 TOPP` (the link error is fixed) but failed fast in `make -j4 pyopenms` with: CMake Error: Not a file: /openms-build/CMakeFiles/VerifyGlobs.cmake CMake Error: Error processing file: /openms-build/CMakeFiles/VerifyGlobs.cmake make: *** [Makefile:11553: cmake_check_build_system] Error 1 `VerifyGlobs.cmake` is generated by cmake for `file(GLOB CONFIGURE_DEPENDS ...)` targets and is consulted by `cmake_check_build_system` at the top of every subsequent `make` invocation. The intermediate cleanup line RUN rm -rf src doc CMakeFiles deleted it, which is fine on the x86 single-pass build (different cmake codepath when PYOPENMS=ON is set in the initial configure, no VerifyGlobs.cmake generated) but breaks the ARM two-pass build. Stop deleting CMakeFiles/ between `make TOPP` and `make pyopenms`. We still drop `src/` and `doc/` for disk savings; keeping CMakeFiles costs only a few hundred MB on the intermediate layer.
eWaterCycle/setup-apptainer@v2 installs apptainer from the upstream .deb asset on the GitHub release. Upstream apptainer only publishes amd64 .debs (verified: every v1.3.x release lists only `apptainer_<ver>_amd64.deb`, no _arm64 / _aarch64 variant). On the ubuntu-24.04-arm runner the action's `apt-get install ./apptainer_*.deb` fails with sudo exit code 100 because the package can't be resolved. Building apptainer from source on the ARM runner would add ~15 minutes and a maintenance surface (Go toolchain, suid configuration) for limited value — HPC SIF consumers remain amd64. Revert test-apptainer to amd64-only and document why. test-nginx and test-traefik still exercise the ARM image via kind, which gives us functional ARM coverage at the docker-runtime level even without apptainer. Side cleanups now that arm64 is gone from this matrix: - artifact name back to a literal `*-amd64-image` (no matrix.arch) - SIF upload gate drops the `matrix.arch == 'amd64'` check
kind/kubectl/helm setup actions fail with "Cache directory '/opt/hostedtoolcache' does not exist". Drop just dotnet/android/ghc (~34 GB) and leave the tool cache in place.
curl exit-22 doesn't tell us whether the pod, service, or ingress is the broken link. Dump pods/logs/ingress/controller logs on failure so the next run surfaces the actual cause.
\`docker load\` + \`kind load docker-image\` keeps the image in both host docker AND each kind node's containerd. With a 5-8 GB image and two kind nodes that's ~25 GB of duplicated storage, which trips the "no space left on device" error in kind's ctr import. Switch to \`kind load image-archive\` so the tar streams directly into each node, and rm the tar after to reclaim /tmp.
503s in test-nginx/test-traefik traced to two issues: 1. The prod overlay maps openms-streamlit -> ghcr.io/openms/streamlit-template:main-full, but the build job was re-tagging the local image as openms-streamlit:test. Rendered manifests pointed at the registry name; kind only had :test loaded; pods stayed ErrImagePull. Retag as :main-full so kind has exactly the ref the manifests use. 2. Three of the four pod specs declare imagePullPolicy: Always; the existing sed only rewrote IfNotPresent. With Always and no registry creds in kind, pods loop on ImagePullBackOff. Extend the sed to catch both.
Add ARM64 (aarch64) multi-architecture Docker build support
…er-fV7HC Adopt the template's modernized build-and-test.yml: split into build-amd64 + build-arm64 (ubuntu-24.04-arm) + create-manifest jobs, per-arch GHCR cache, image-artifact deposit for downstream tests, and the extended apptainer/nginx/traefik kind-cluster test suite covering both architectures. FLASHApp customizations applied on top of the template workflow: - Trigger branches main -> develop - Matrix reduced to variant=full only (FLASHApp dropped the simple variant in d1b3de0); test-nginx/test-traefik arch matrices keep amd64 + arm64 - Retag-for-kind targets ghcr.io/openms/flashapp:latest to match the prod overlay's newName/newTag - Latest tag rule keyed to refs/heads/develop Keep FLASHApp's OpenMS build setup: Dockerfile and Dockerfile.arm unchanged (Vue JS stage, t0mdavid-m/OpenMS.git#FVdeploy fork, FLASHApp ARGs, two-pass cmake). Drop template's Dockerfile_simple.arm. publish-docker-images.yml (release-tag flow) left untouched.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughSplits image builds into explicit amd64 and arm64 jobs that upload per-arch tar artifacts, adds a non-PR manifest job to create multi-arch lists, and updates apptainer/nginx/traefik tests to consume arch-specific artifacts via kind image loading with enhanced failure diagnostics. ChangesMulti-architecture build and test orchestration
Sequence DiagramsequenceDiagram
participant build_amd64 as build-amd64
participant build_arm64 as build-arm64
participant create_manifest as create-manifest
participant GHCR as GHCR registry
par amd64 path
build_amd64->>build_amd64: Build linux/amd64, tag -amd64
build_amd64->>build_amd64: Upload -amd64 artifact
and arm64 path
build_arm64->>build_arm64: Free disk space
build_arm64->>build_arm64: Build linux/arm64, tag -arm64
build_arm64->>build_arm64: Upload -arm64 artifact
end
build_amd64->>create_manifest: await both builds
build_arm64->>create_manifest: await both builds
create_manifest->>create_manifest: Iterate manifest tags
create_manifest->>GHCR: Create and push multi-arch manifests
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build-and-test.yml:
- Line 149: Update every GitHub Actions "uses:" line that uses a mutable tag
(e.g., actions/checkout@v4, docker/*`@v3/`@v5, actions/*`@v4`,
eWaterCycle/setup-apptainer@v2, helm/kind-action@v1, azure/setup-helm@v4) to the
corresponding full 40-character commit SHA; find each occurrence of those module
references in the workflow and replace the tag form with the specific commit SHA
for that release so the CI is pinned to immutable versions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3af9f83d-3238-4113-b78a-2217cf5abd22
📒 Files selected for processing (1)
.github/workflows/build-and-test.yml
| sudo apt-get clean | ||
| df -h | ||
|
|
||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List every action reference in this workflow that is not pinned to a full 40-char SHA.
rg -nP '^\s*uses:\s+\S+@(?![0-9a-fA-F]{40}\b)\S+' .github/workflows/build-and-test.ymlRepository: OpenMS/FLASHApp
Length of output: 1117
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/build-and-test.yml"
echo "== checkout uses lines =="
rg -n "uses:\s+actions/checkout@" "$FILE" || true
echo
echo "== line context around 140-170 =="
nl -ba "$FILE" | sed -n '140,175p'
echo
echo "== all sed -E occurrences (with a bit of surrounding context) =="
rg -n "sed -E" "$FILE" || true
rg -n -C 3 "sed -E" "$FILE" || true
echo
echo "== any imagePullPolicy rewrite lines =="
rg -n "imagePullPolicy" "$FILE" || true
rg -n -C 3 "imagePullPolicy" "$FILE" || true
echo
echo "== full list of all non-SHA-pinned uses lines (reuse same regex) =="
rg -nP '^\s*uses:\s+\S+@(?![0-9a-fA-F]{40}\b)\S+' "$FILE"Repository: OpenMS/FLASHApp
Length of output: 408
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/build-and-test.yml"
echo "== line context around 140-180 =="
awk 'NR>=140 && NR<=180 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== line context around 190-270 =="
awk 'NR>=190 && NR<=270 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== all sed -E occurrences with nearby lines =="
# Print only the matching line plus next 6 lines for each occurrence
awk '
/sed -E/ {print "---- match at line " NR " ----"; print NR ":" $0; for(i=1;i<=6;i++){getline; print (NR+i) ":" $0}}
' "$FILE"
echo
echo "== imagePullPolicy occurrences with nearby lines =="
awk '
/imagePullPolicy/ {print "---- match at line " NR " ----"; print NR ":" $0; for(i=1;i<=6;i++){getline; print (NR+i) ":" $0}}
' "$FILE"
echo
echo "== non-SHA-pinned uses lines (regex from prior script) =="
rg -nP '^\s*uses:\s+\S+@(?![0-9a-fA-F]{40}\b)\S+' "$FILE"Repository: OpenMS/FLASHApp
Length of output: 8882
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/build-and-test.yml"
echo "== line context around 140-180 =="
awk 'NR>=140 && NR<=180 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== line context around 190-270 =="
awk 'NR>=190 && NR<=270 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== all sed -E occurrences with nearby lines =="
# Print only the matching line plus next 6 lines for each occurrence
awk '
/sed -E/ {print "---- match at line " NR " ----"; print NR ":" $0; for(i=1;i<=6;i++){getline; print (NR+i) ":" $0}}
' "$FILE"
echo
echo "== imagePullPolicy occurrences with nearby lines =="
awk '
/imagePullPolicy/ {print "---- match at line " NR " ----"; print NR ":" $0; for(i=1;i<=6;i++){getline; print (NR+i) ":" $0}}
' "$FILE"
echo
echo "== non-SHA-pinned uses lines (regex from prior script) =="
rg -nP '^\s*uses:\s+\S+@(?![0-9a-fA-F]{40}\b)\S+' "$FILE"Repository: OpenMS/FLASHApp
Length of output: 8882
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/build-and-test.yml"
# Extract the exact sed -E replacement expression(s) from the workflow and test them on sample inputs.
expr=$(rg -o "sed -E 's\|imagePullPolicy: \\(IfNotPresent\\\\\|Always\\)\\|imagePullPolicy: Never\\|g'" "$FILE" | head -n 1 || true)
if [[ -z "${expr}" ]]; then
# Fallback: use the literal expression observed in the file
expr="sed -E 's|imagePullPolicy: (IfNotPresent\\|Always)|imagePullPolicy: Never|g'"
fi
echo "Using expression: $expr"
echo
printf '%s\n' \
"imagePullPolicy: IfNotPresent" \
"imagePullPolicy: Always" \
"imagePullPolicy: IfNotPresent|Always" | while read -r line; do
printf 'IN : %s\n' "$line"
out=$(printf '%s\n' "$line" | bash -lc "$expr")
printf 'OUT: %s\n\n' "$out"
doneRepository: OpenMS/FLASHApp
Length of output: 669
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/build-and-test.yml"
# Match both `uses:` and `- uses:` lines; flag anything not pinned to a full 40-char SHA.
rg -nP '^\s*(?:-\s*)?uses:\s+\S+@(?![0-9a-fA-F]{40}\b)\S+' "$FILE"Repository: OpenMS/FLASHApp
Length of output: 1343
Pin CI uses: references in .github/workflows/build-and-test.yml to full commit SHAs
The workflow has many uses: entries using mutable version tags (e.g., actions/checkout@v4, docker/*@v3/@v5, actions/*@v4``, eWaterCycle/setup-apptainer@v2, `helm/kind-action@v1`, `azure/setup-helm@v4`); pin each to its 40-char commit SHA.
The sed -E pattern used to rewrite imagePullPolicy correctly matches both IfNotPresent and Always and replaces them with Never, so that concern doesn’t apply here.
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 149-149: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 149-149: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-and-test.yml at line 149, Update every GitHub
Actions "uses:" line that uses a mutable tag (e.g., actions/checkout@v4,
docker/*`@v3/`@v5, actions/*`@v4`, eWaterCycle/setup-apptainer@v2,
helm/kind-action@v1, azure/setup-helm@v4) to the corresponding full 40-character
commit SHA; find each occurrence of those module references in the workflow and
replace the tag form with the specific commit SHA for that release so the CI is
pinned to immutable versions.
…fest create-manifest fails with "ghcr.io/openms/streamlit-template:main-full-amd64 is a manifest list" because docker/build-push-action v5 adds a provenance attestation by default, which buildx packs as a manifest list (image + attestation entries). docker manifest create rejects manifest lists as components. build-arm64 already sets provenance: false for the same reason; mirror that on the amd64 path so both per-arch tags are flat image manifests that can be merged into the multi-arch manifest.
test-traefik failed unpacking the OpenMS image into containerd overlayfs on the kind node: no space left on device while extracting layer /usr/include/boost/json/object.hpp The image is loaded into BOTH kind nodes (control-plane + worker) so the OpenMS source / boost headers / pyopenms layers consume ~2x their unpacked size. The previous cleanup only removed dotnet, android and ghc, leaving the runner well short of what kind's double-extraction needs. Drop the next biggest unused chunks (~16 GB more): - /usr/local/.ghcup (~2.7 GB) - /usr/share/swift (~2 GB) - /usr/local/share/boost (~1.5 GB) - /opt/hostedtoolcache/CodeQL (~5 GB; keep the rest of hostedtoolcache since helm/kind-action and setup-kubectl cache binaries there) - docker image prune (~5-7 GB of pre-installed runner images) Applied uniformly to build-arm64 / test-apptainer / test-nginx / test-traefik since they all eat the same image.
Summary
This PR adds native ARM64 (aarch64) Docker image builds alongside existing AMD64 builds, with automatic multi-arch manifest creation for transparent architecture selection on pull. The workflow now produces per-architecture tags (
-amd64,-arm64) that are merged into unified manifest lists under the original tag scheme, allowing existing consumers (k8s overlays, docker-compose, direct pulls) to work unchanged while gaining ARM support.Key Changes
Split build job into architecture-specific jobs:
build-amd64: Runs onubuntu-latest, produces*-amd64tags, uses standardDockerfilebuild-arm64: Runs on nativeubuntu-24.04-armrunner, produces*-arm64tags, usesDockerfile.arm(with aarch64-specific miniforge installer and THIRDPARTY paths)New
create-manifestjob: Stitches per-arch tags into multi-arch manifest lists under the original tag scheme (<ref>-<variant>,latest), enabling transparent architecture selection ondocker pullUpdated test jobs for ARM support:
test-nginxandtest-traefik: Now run on bothubuntu-latest(amd64) andubuntu-24.04-arm(arm64) via matrix strategytest-apptainer: Remains amd64-only (upstream apptainer does not publish arm64 .deb assets)kind load image-archiveinstead ofdocker load+kind load docker-imageto save ~5-8 GB of disk spaceDisk space optimizations:
build-arm64,test-nginx, andtest-traefikjobs (removes/usr/share/dotnet,/usr/local/lib/android,/opt/ghc)Improved image tagging and retagging:
ghcr.io/openms/flashapp:latest) instead of a temporary local name, with clearer comments explaining the kustomize overlay contractEnhanced test failure diagnostics:
test-nginxandtest-traefikjobs for better debugging (nodes, pods, logs, ingress/ingressroute, services, controller logs)Minor sed regex fix: Updated imagePullPolicy sed patterns to handle both
IfNotPresentandAlwaysvaluesImplementation Details
Dockerfile.arm(separate from standardDockerfile) to swap the miniforge installer to aarch64 and conditionally copy THIRDPARTY/Linux/aarch64cache:*-amd64,cache:*-arm64) to avoid cross-architecture pollutiondocker manifest create --amendfor idempotency across workflow_dispatch rerunscreate-manifestjob)https://claude.ai/code/session_01XXcpjdQC93TA4xKHeZPPAp
Summary by CodeRabbit