diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 83de44c..efa89ef 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -92,8 +92,14 @@ jobs: BASELINE_HUMAN="${{ steps.baseline-size.outputs.human }}" BASELINE_BYTES=${{ steps.baseline-size.outputs.bytes }} DIFF_BYTES=$(( PR_BYTES - BASELINE_BYTES )) - if [ "$DIFF_BYTES" -ge 0 ]; then SIGN="+"; else SIGN=""; fi - DIFF_HUMAN=$(numfmt --to=iec --suffix=B "$DIFF_BYTES" 2>/dev/null || echo "${DIFF_BYTES} B") + if [ "$DIFF_BYTES" -gt 0 ]; then + SIGN="+" + ABS_DIFF=$DIFF_BYTES + else + SIGN="" + ABS_DIFF=$(( -DIFF_BYTES )) + fi + DIFF_HUMAN=$(numfmt --to=iec --suffix=B "$ABS_DIFF" 2>/dev/null || echo "${ABS_DIFF} B") fi { @@ -121,11 +127,7 @@ jobs: echo "pr: $PR_HUMAN ($PR_BYTES bytes)" echo "pr-human=${PR_HUMAN}" >> "$GITHUB_OUTPUT" - if [ "$DIFF_BYTES" -ge 0 ]; then - echo "diff-human=+${DIFF_HUMAN}" >> "$GITHUB_OUTPUT" - else - echo "diff-human=${DIFF_HUMAN}" >> "$GITHUB_OUTPUT" - fi + echo "diff-human=${SIGN}${DIFF_HUMAN}" >> "$GITHUB_OUTPUT" - name: Comment on PR if: github.event_name == 'pull_request' diff --git a/Dockerfile b/Dockerfile index a52afad..108d86b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,16 +4,14 @@ # Ubuntu base with a broad, familiar dev toolchain. All configuration is env-driven # (OPENCODE_SERVER_PASSWORD, OPENCODE_CORS_ORIGIN, OPENCODE_PORT). The entire home # directory (/home/opencode) is the persistent mount point; the active project -# lives under ~/workspace. Node lives in /opt/n (outside home) so it's never -# shadowed when a volume mounts over the home directory. +# lives under ~/workspace. # # Three-stage build: # base — apt packages, user, sudo, init — shared by builder and final -# builder — fetches relocatable toolchains (Node, opencode, Homebrew) +# builder — fetches relocatable toolchains (opencode, Homebrew, mise) # final — copies in runtimes from builder; carries only runtime layers ARG OPENCODE_VERSION=0.0.0 -ARG NODE_PREFIX=/opt/n # --------------------------------------------------------------------------- # base: common runtime layer (apt, user, sudo, init) @@ -23,20 +21,11 @@ FROM ubuntu:26.04 AS base ENV DEBIAN_FRONTEND=noninteractive # General dev toolchain: VCS, build tools, languages, CLI utilities. -# Also installs GitHub CLI via its official apt repo. RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates curl wget git openssh-client unzip xz-utils \ - build-essential pkg-config \ - python3 python3-pip python3-venv ruby \ - ripgrep fd-find jq less nano vim-tiny \ - sudo tini open-iscsi tzdata locales \ - && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - > /etc/apt/sources.list.d/github-cli.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends gh \ - && rm -rf /var/lib/apt/lists/* \ + ca-certificates curl git openssh-client unzip xz-utils \ + build-essential jq pkg-config \ + less sudo tini tzdata locales \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \ && userdel --remove ubuntu 2>/dev/null || true; \ groupdel ubuntu 2>/dev/null || true; \ groupadd --gid 1000 opencode \ @@ -57,10 +46,6 @@ RUN chmod 0755 /usr/local/bin/entrypoint.sh # --------------------------------------------------------------------------- FROM base AS builder -ARG NODE_PREFIX -ENV N_PREFIX=${NODE_PREFIX} -ENV PATH=${NODE_PREFIX}/bin:${PATH} - # 1. Homebrew — the install script URL is stable; brew releases rarely # invalidate the layer once installed. RUN mkdir -p /home/linuxbrew \ @@ -68,26 +53,23 @@ RUN mkdir -p /home/linuxbrew \ && sudo -u opencode NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \ && sudo -u opencode /home/linuxbrew/.linuxbrew/bin/brew cleanup --prune=all \ && sudo -u opencode rm -rf "$(sudo -u opencode /home/linuxbrew/.linuxbrew/bin/brew --cache)" \ - && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/homebrew/homebrew-core \ && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/test \ - && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/cask \ && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/bundle/ruby/*/cache \ && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/bundle/ruby/*/doc \ - && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Homebrew/vendor/portable-ruby \ && rm -rf /home/linuxbrew/.linuxbrew/share/man \ && rm -rf /home/linuxbrew/.linuxbrew/share/doc \ - && rm -rf /home/linuxbrew/.linuxbrew/share/zsh + && rm -rf /home/linuxbrew/.linuxbrew/share/zsh \ + && rm -rf /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/homebrew/homebrew-core -# 2. Node.js via `n` — changes when the upstream LTS version bumps -RUN curl -fsSL -o /usr/local/bin/n https://raw.githubusercontent.com/tj/n/master/bin/n \ - && chmod 0755 /usr/local/bin/n \ - && mkdir -p "${N_PREFIX}" \ - && n install --cleanup current \ - && node --version && npm --version +# 1.5. mise — dev tool manager; pre-approved tools defined in the global config +# auto-install via Homebrew backend on first use at runtime. +RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \ + && mkdir -p /opt/mise \ + && MISE_DATA_DIR=/opt/mise mise plugins install brew https://github.com/woutermont/mise-brew ARG OPENCODE_VERSION -# 3. opencode server binary — changes on every version bump (most frequent) +# 2. opencode server binary — changes on every version bump (most frequent) RUN curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash \ && (cp /root/.opencode/bin/opencode /opt/opencode 2>/dev/null \ || cp "$HOME/.opencode/bin/opencode" /opt/opencode) \ @@ -99,20 +81,39 @@ RUN curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash # --------------------------------------------------------------------------- FROM base -ARG NODE_PREFIX -ENV N_PREFIX=${NODE_PREFIX} -ENV PATH=${N_PREFIX}/bin:/home/opencode/.local/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH} +ENV PATH=/home/opencode/.local/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/opt/auto-install-shims:${PATH} +ENV HOMEBREW_NO_AUTO_UPDATE=1 +ENV HOMEBREW_INSTALL_FROM_API=1 +ENV MISE_DATA_DIR=/opt/mise +ENV MISE_ALWAYS_INSTALL=1 # Runtimes copied from builder (most-stable first so frequent version # bumps don't invalidate cache for the other layers). COPY --from=builder --chown=opencode:opencode /home/linuxbrew /home/linuxbrew -COPY --from=builder --chown=opencode:opencode ${NODE_PREFIX} ${NODE_PREFIX} COPY --from=builder /opt/opencode /usr/local/bin/opencode -# Verify runtimes and set up login-shell PATH -RUN node --version && npm --version && opencode --version \ - && printf 'export N_PREFIX=%s\nfor d in "$N_PREFIX/bin" "$HOME/.local/bin" "/home/linuxbrew/.linuxbrew/bin" "/home/linuxbrew/.linuxbrew/sbin"; do case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH";; esac; done\nexport PATH\n' "${N_PREFIX}" > /etc/profile.d/node-path.sh \ - && chmod 0644 /etc/profile.d/node-path.sh +# Mise — dev tool manager; auto-installs tools defined in the global config. +COPY --from=builder /usr/local/bin/mise /usr/local/bin/mise +COPY --from=builder --chown=opencode:opencode /opt/mise /opt/mise +COPY mise-config.toml /etc/mise/config.toml + +# Verify runtime and set up login-shell PATH and auto-install handler +RUN opencode --version \ + && printf 'for d in "$HOME/.local/bin" "/home/linuxbrew/.linuxbrew/bin" "/home/linuxbrew/.linuxbrew/sbin"; do case ":$PATH:" in *":$d:"*) ;; *) PATH="$d:$PATH";; esac; done\nexport PATH\n' > /etc/profile.d/brew-path.sh \ + && chmod 0644 /etc/profile.d/brew-path.sh \ + && printf '\neval "$(mise activate bash)"\n' >> /home/opencode/.bashrc \ + && printf '\neval "$(mise activate zsh)"\n' >> /home/opencode/.zshrc \ + && mkdir -p /home/opencode/.config/fish \ + && printf '\nmise activate fish | source\n' >> /home/opencode/.config/fish/config.fish \ + && printf '\neval "$(mise activate sh)"\n' >> /home/opencode/.profile \ + && mkdir -p /opt/auto-install-shims \ + && grep -E '^\s*"' /etc/mise/config.toml | while IFS='=' read -r key value; do \ + key="$(echo "$key" | tr -d ' "')" \ + && shim="${key#*:}" \ + && printf '#!/usr/bin/env bash\nexec /usr/local/bin/mise exec "%s" -- %s "$@"\n' "$key" "$shim" > "/opt/auto-install-shims/$shim" \ + && chmod 0755 "/opt/auto-install-shims/$shim"; \ + done \ + && chown -R opencode:opencode /opt/auto-install-shims USER opencode ENV HOME=/home/opencode diff --git a/README.md b/README.md index 71707f3..36603ae 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,33 @@ A general-purpose Ubuntu Docker image for running [opencode](https://opencode.ai | **Base OS** | ubuntu:26.04 | | **User** | `opencode` (uid/gid 1000), passwordless sudo | | **opencode** | Pinned in `version.txt` as `OPENCODE_VERSION` build arg | -| **Node.js** | Current LTS via `n`, installed to `/opt/n` (outside home) | -| **Python 3** | pip, venv | | **Build tools** | `build-essential`, `pkg-config` (for native npm addons, pip source builds) | -| **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH for all users | -| **CLI utilities** | git, curl, wget, gh (GitHub CLI), jq, ripgrep, fd-find, vim, nano, less, unzip, ssh client | +| **Python 3** | Lazy-installed via mise (see table below) | +| **Homebrew** | Linux-native Homebrew (`/home/linuxbrew/.linuxbrew`) — `brew` on PATH | +| **mise** | Dev tool manager — tools listed below install on first use via `brew` backend | +| **CLI utilities** | git, curl, jq, less, unzip, ssh client | | **Init** | tini as PID 1 (zombie reaping, clean shutdown) | +### Lazy-installed tools + +These tools install on first use (via mise → Homebrew): + +| Tool | Command | Backend | +|---|---|---| +| GitHub CLI | `gh` | brew | +| GitLab CLI | `glab` | brew | +| Ruby | `ruby` | brew | +| ripgrep | `rg` | brew | +| fd | `fd` | brew | +| Wget | `wget` | brew | +| Vim | `vim` | brew | +| Micro | `micro` | brew | +| Nano | `nano` | brew | +| Python 3 | `python3` | brew | +| Node.js | `node` | brew | + +The image ships with a system config at `/etc/mise/config.toml` with these pre-approved tools. Users can add or override tools by creating `~/.config/mise/config.toml` — mise merges both. + ## Usage ### Quick start @@ -83,4 +103,5 @@ Fetches the latest release from [anomalyco/opencode](https://github.com/anomalyc - The root filesystem is ephemeral; mount `/home/opencode` as the persistent volume for all user data (dotfiles, config, projects). The `~/workspace` subdirectory is the default workdir. - `~/.local/bin` is on PATH and user-writable, useful for dropping custom tools at runtime. - Node version can be switched at runtime with `n ` (e.g. `n lts`). -- Homebrew is installed under `/home/linuxbrew/.linuxbrew` (outside the persistent volume) and is usable immediately by the `opencode` user. +- Homebrew is installed under `/home/linuxbrew/.linuxbrew` (outside the persistent volume). It uses its bundled portable Ruby — no system Ruby needed. +- **Lazy-installed tools** (see table above): run any listed tool and mise auto-installs it via Homebrew on first use. The image ships defaults in `/etc/mise/config.toml`; create `~/.config/mise/config.toml` to add your own — mise merges both. diff --git a/mise-config.toml b/mise-config.toml new file mode 100644 index 0000000..356b97e --- /dev/null +++ b/mise-config.toml @@ -0,0 +1,12 @@ +[tools] +"brew:gh" = "latest" +"brew:glab" = "latest" +"brew:ruby" = "latest" +"brew:ripgrep" = "latest" +"brew:fd" = "latest" +"brew:wget" = "latest" +"brew:vim" = "latest" +"brew:micro" = "latest" +"brew:nano" = "latest" +"brew:python" = "latest" +"brew:node" = "latest"