From 2c889b472e667295c134bd9dbeba675c63793bf8 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Wed, 29 Apr 2026 10:14:42 +0200 Subject: [PATCH] Add --retry to all Dockerfile curl invocations The first v1.14.29b build attempt failed with an HTTP 502 from GitHub's release CDN mid-download of zoxide. Single-shot curl had no retry, so one transient 502 failed the entire OMOS build. - curl --retry 5 --retry-delay 5 --retry-all-errors on every tool download (both -fsSL GETs and -sI HEAD redirect lookups). - [ -n "$V" ] assertion after each version-resolution step, so a failed HEAD lookup fails fast with a clear message instead of producing an empty tag that then 404s on the download URL. - Same hardening applied to the optional Go install block and the nodesource setup_22.x pipe. --- CHANGELOG.md | 8 ++++++-- Dockerfile | 58 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1dfbed..6d87f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,17 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a ## v1.14.29b — 2026-04-29 -**Fix OMOS `bunx` detection.** +**Fix OMOS `bunx` detection + CI build reliability.** - **Fix:** `entrypoint-user.sh` checked `command -v bunx` to gate the OMOS auto-install, but the OMOS image only ships the `bun` binary — upstream's bun installer never creates a `bunx` symlink and neither did our Dockerfile. The check always failed on a fresh OMOS image, so `bun x oh-my-opencode-slim@latest install` never ran and first-start OMOS setup would have printed `ENABLE_OMOS=true but bun is not installed.` even though bun was right there. Latent until now because the only exercised path had a persisted `oh-my-opencode-slim.json` from a prior install. - Changed the gate to `command -v bun`. - Changed both install invocations from `bunx oh-my-opencode-slim@latest install ...` to `bun x oh-my-opencode-slim@latest install ...`. - - Added `ln -sf bun /usr/local/bin/bunx` to the Dockerfile's OMOS block so interactive users can still type `bunx` by habit, and verified the symlink during build. + - Added `ln -sf bun /usr/local/bin/bunx` to the Dockerfile's OMOS block so interactive users can still type `bunx` by habit, and verified the symlink at build time (`test -L /usr/local/bin/bunx`). - Smoke test now asserts the `bunx` symlink is present on the OMOS variant. +- **Fix:** CI build robustness against transient GitHub/Gitea CDN failures. The first attempt at building v1.14.29b tripped on a single HTTP 502 from GitHub's release CDN mid-download (`zoxide-0.9.9-x86_64-unknown-linux-musl.tar.gz`), failing the entire OMOS build with no retry. Fix applied to every tool-download curl in the Dockerfile: + - `curl --retry 5 --retry-delay 5 --retry-all-errors` on both the `-fsSL` GET requests and the `-sI` HEAD requests used for `/releases/latest` redirect resolution. 5 attempts with 5 s back-off eats most transient CDN hiccups without failing the build. + - Added `[ -n "$V" ]` assertion after each version-resolution step. If the HEAD redirect ever fails to produce a tag name, the build fails fast with an empty-version message rather than trying to download `.../v//...` and producing a confusing 404. + - Same hardening applied to the optional Go install block (go.dev JSON feed + tarball download) and the nodesource apt-repo setup script. ## v1.14.29 — 2026-04-28 diff --git a/Dockerfile b/Dockerfile index 3f9b332..c6121e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,11 +68,12 @@ ARG GOSU_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${GOSU_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing gosu ${V}" && \ - curl -fsSL "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \ chmod +x /usr/local/bin/gosu && \ gosu --version @@ -81,11 +82,12 @@ ARG FZF_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${FZF_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing fzf ${V}" && \ - curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \ fzf --version # git-lfs — Git Large File Storage @@ -93,11 +95,12 @@ ARG GIT_LFS_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${GIT_LFS_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing git-lfs ${V}" && \ - curl -fsSL "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \ install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \ rm -rf /tmp/git-lfs-${V} && \ git lfs install --system && \ @@ -108,11 +111,12 @@ ARG NVIM_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \ V="${NVIM_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing neovim ${V}" && \ - curl -fsSL "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \ ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \ nvim --version | head -1 @@ -121,11 +125,12 @@ ARG BAT_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ V="${BAT_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing bat ${V}" && \ - curl -fsSL "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \ rm -rf /tmp/bat-v${V}-* && \ bat --version @@ -135,11 +140,12 @@ ARG EZA_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ V="${EZA_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing eza ${V}" && \ - curl -fsSL "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \ eza --version | head -1 # zoxide — smarter cd command @@ -147,11 +153,12 @@ ARG ZOXIDE_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ V="${ZOXIDE_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing zoxide ${V}" && \ - curl -fsSL "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \ zoxide --version # uv — fast Python package manager (replaces pip, venv, pyenv) @@ -160,11 +167,12 @@ ARG UV_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ V="${UV_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing uv ${V}" && \ - curl -fsSL "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \ install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \ install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \ rm -rf /tmp/uv-* && \ @@ -198,7 +206,7 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \ # rustup-init -y && source ~/.cargo/env # Toolchains persist via devbox-rustup and devbox-cargo volumes. RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ - curl -fsSL "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \ chmod +x /usr/local/bin/rustup-init # gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com) @@ -206,11 +214,12 @@ ARG GITEA_MCP_VERSION=latest RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \ V="${GITEA_MCP_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -sI "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ + V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \ fi && \ V="${V#v}" && \ + [ -n "$V" ] && \ echo "Installing gitea-mcp ${V}" && \ - curl -fsSL "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \ | tar -xz -C /usr/local/bin/ gitea-mcp && \ chmod +x /usr/local/bin/gitea-mcp && \ gitea-mcp --version @@ -226,7 +235,7 @@ ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}" # ── Node.js (required for opencode v1.x install + MCP servers) ────── ARG NODE_VERSION=22 -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \ +RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ rm -rf /var/lib/apt/lists/* @@ -241,7 +250,7 @@ RUN ARCH=$(case "${TARGETARCH}" in \ arm64) echo "aarch64" ;; \ *) echo "x86_64" ;; \ esac) && \ - curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \ unzip -q /tmp/awscli.zip -d /tmp && \ /tmp/aws/install && \ rm -rf /tmp/aws /tmp/awscli.zip && \ @@ -257,11 +266,12 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \ GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \ V="${GO_VERSION}" && \ if [ "$V" = "latest" ]; then \ - V=$(curl -fsSL "https://go.dev/dl/?mode=json" | \ + V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \ awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \ fi && \ + [ -n "$V" ] && \ echo "Installing Go ${V}" && \ - curl -fsSL "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \ ln -s /usr/local/go/bin/go /usr/local/bin/go && \ ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \ fi @@ -280,7 +290,7 @@ RUN if [ "${INSTALL_OMOS}" = "true" ]; then \ elif [ "$ARCH" = "aarch64" ]; then \ BUN_ARCH="aarch64"; \ fi && \ - curl -fsSL "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \ + curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \ unzip -o /tmp/bun.zip -d /tmp/bun && \ mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \ chmod +x /usr/local/bin/bun && \