Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a208b073b0 | |||
| a803fe4653 | |||
| 79b697dea0 | |||
| 3e3abc8672 | |||
| 59e58a9d00 | |||
| 26ce9aa490 | |||
| 3d4e739529 | |||
| a6b0b59946 | |||
| fc74a8f906 | |||
| 5a2d06340e | |||
| 23894bc19f | |||
| f0918ba915 |
@@ -31,6 +31,31 @@ WORKSPACE_PATH=~/projects
|
|||||||
# Path to SSH keys on host
|
# Path to SSH keys on host
|
||||||
SSH_KEY_PATH=~/.ssh
|
SSH_KEY_PATH=~/.ssh
|
||||||
|
|
||||||
|
# ── Skillset (agent skills and instructions) ─────────────────────────
|
||||||
|
# If you have a skillset repo, the entrypoint auto-deploys skills and
|
||||||
|
# instructions on container start using relative symlinks (portable
|
||||||
|
# across host/container).
|
||||||
|
#
|
||||||
|
# Detection is automatic if the skillset lives directly at the workspace
|
||||||
|
# root (i.e. WORKSPACE_PATH/skillset → /workspace/skillset in container).
|
||||||
|
#
|
||||||
|
# If the skillset lives in a subdirectory of your workspace, set
|
||||||
|
# SKILLSET_CONTAINER_PATH to its location *inside the container*. This
|
||||||
|
# is determined by the workspace mount: whatever is at
|
||||||
|
# WORKSPACE_PATH/<subpath> on the host becomes /workspace/<subpath>
|
||||||
|
# in the container.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# Host skillset at ~/projects/skillset → already at /workspace/skillset (auto-detected, no config needed)
|
||||||
|
# Host skillset at ~/projects/tools/skillset → SKILLSET_CONTAINER_PATH=/workspace/tools/skillset
|
||||||
|
# Host skillset at ~/projects/local/skillset → SKILLSET_CONTAINER_PATH=/workspace/local/skillset
|
||||||
|
#
|
||||||
|
# Alternatively, mount the skillset repo at a dedicated path using the
|
||||||
|
# SKILLSET_PATH volume in docker-compose.yml (see comments there). In
|
||||||
|
# that case the entrypoint finds it at ~/skillset automatically.
|
||||||
|
#
|
||||||
|
# SKILLSET_CONTAINER_PATH=
|
||||||
|
|
||||||
# ── Locale (defaults to en_US.UTF-8) ─────────────────────────────────
|
# ── Locale (defaults to en_US.UTF-8) ─────────────────────────────────
|
||||||
# LANG=sv_SE.UTF-8
|
# LANG=sv_SE.UTF-8
|
||||||
# LANGUAGE=sv_SE:sv
|
# LANGUAGE=sv_SE:sv
|
||||||
|
|||||||
@@ -5,8 +5,56 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
|
# Serialize concurrent runs of the same workflow on the same ref so the
|
||||||
|
# build jobs can't race `docker system prune` in the smoke gates
|
||||||
|
# (pruning from one job can nuke another job's in-flight buildx cache).
|
||||||
|
# cancel-in-progress: false — tag pushes are release events, we never
|
||||||
|
# want to silently drop one.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
# Plain progress output from BuildKit — critical for diagnosing stalls
|
||||||
|
# inside arm64-under-QEMU builds where the default collapsed progress UI
|
||||||
|
# hides which step is stuck.
|
||||||
|
env:
|
||||||
|
BUILDKIT_PROGRESS: plain
|
||||||
|
|
||||||
|
# Runner disk pressure notes:
|
||||||
|
# Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host
|
||||||
|
# with limited overlay space (~40 GB, often 70%+ used at start). Two jobs
|
||||||
|
# per variant:
|
||||||
|
# * smoke gate (amd64 only, `load: true` into local dockerd for smoke
|
||||||
|
# testing) — peak disk = tarball + unpacked image + buildx cache. The
|
||||||
|
# `Reclaim runner disk` step below strips catthehacker-resident
|
||||||
|
# toolchains and prunes stale docker state before buildx starts.
|
||||||
|
# * build job (amd64 + arm64, `push-by-digest` streaming directly to
|
||||||
|
# Docker Hub, no local unpack). Peak disk on push-by-digest is
|
||||||
|
# BuildKit's content store only — much smaller than `load: true`.
|
||||||
|
# `docker/build-push-action@v7` with comma-separated platforms
|
||||||
|
# publishes a proper multi-arch manifest in one step.
|
||||||
|
#
|
||||||
|
# Why not matrix + digest artifacts?
|
||||||
|
# An earlier revision split each arch into its own matrix job and used
|
||||||
|
# `actions/upload-artifact` to pass digests to a merge job. On Gitea
|
||||||
|
# Actions, `actions/{upload,download}-artifact@v4+` fails with
|
||||||
|
# `GHESNotSupportedError` — v4 relies on a GitHub-specific Artifact
|
||||||
|
# API that Gitea doesn't implement. Rather than downgrade to @v3 (the
|
||||||
|
# last Gitea-compatible release) we collapsed back to single-job
|
||||||
|
# multi-arch push. The matrix only helps when the build literally
|
||||||
|
# cannot fit on one runner, which push-by-digest + reclaim no longer
|
||||||
|
# hits for this image.
|
||||||
|
#
|
||||||
|
# Gitea Actions gotchas baked into this file:
|
||||||
|
# * `actions/{upload,download}-artifact` must stay at @v3 on Gitea.
|
||||||
|
# * Step scripts run under /bin/sh (dash) — no bash-isms like
|
||||||
|
# ${VAR//a/b}. Use `tr` or explicit `shell: bash`.
|
||||||
|
# * `docker/build-push-action@v7` with `platforms: a,b` works for
|
||||||
|
# multi-arch push natively; no matrix/merge dance needed.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-base:
|
# ── Smoke test (amd64 only, gates the push jobs) ────────────────────
|
||||||
|
smoke-base:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -15,30 +63,41 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Force IPv4 for Docker Hub
|
- name: Force IPv4 for Docker Hub
|
||||||
run: |
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
# Prefer IPv4 to avoid intermittent IPv6 connectivity failures
|
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
# See docker-publish.yml preamble. `load: true` peak disk = tarball
|
||||||
uses: docker/setup-qemu-action@v4
|
# + unpacked image + buildx cache; the image now crosses the 40 GB
|
||||||
|
# runner overlay's starting headroom. Strip catthehacker-resident
|
||||||
|
# toolchains and any stale docker state up front.
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
driver-opts: network=host
|
driver-opts: network=host
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
- name: Build and load amd64 image for smoke test
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
@@ -49,20 +108,9 @@ jobs:
|
|||||||
tags: opencode-devbox:smoke-base
|
tags: opencode-devbox:smoke-base
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
run: |
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
||||||
bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
|
||||||
|
|
||||||
- name: Build and push (base)
|
smoke-omos:
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest
|
|
||||||
|
|
||||||
build-omos:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -71,30 +119,37 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Force IPv4 for Docker Hub
|
- name: Force IPv4 for Docker Hub
|
||||||
run: |
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
# Prefer IPv4 to avoid intermittent IPv6 connectivity failures
|
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Reclaim runner disk
|
||||||
uses: docker/setup-qemu-action@v4
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
driver-opts: network=host
|
driver-opts: network=host
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
- name: Build and load amd64 image for smoke test
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
@@ -107,10 +162,133 @@ jobs:
|
|||||||
tags: opencode-devbox:smoke-omos
|
tags: opencode-devbox:smoke-omos
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
run: |
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
||||||
bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
|
||||||
|
|
||||||
- name: Build and push (omos)
|
# ── Multi-arch push (single job per variant, comma-separated platforms) ─
|
||||||
|
build-base:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-base
|
||||||
|
timeout-minutes: 90
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
# Lighter reclaim than the smoke-gate version: push-by-digest
|
||||||
|
# doesn't write to host dockerd, so `docker system prune` adds
|
||||||
|
# little. BuildKit cache from prior runs is the thing to clear.
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
with:
|
||||||
|
platforms: arm64
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push (multi-arch)
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
|
||||||
|
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest
|
||||||
|
|
||||||
|
build-omos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: smoke-omos
|
||||||
|
timeout-minutes: 90
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Force IPv4 for Docker Hub
|
||||||
|
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
with:
|
||||||
|
platforms: arm64
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push (multi-arch)
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -46,6 +46,34 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
# The runner's overlay disk starts ~70% full. `load: true` peak disk
|
||||||
|
# is tarball + unpacked image + buildx cache, which tips it over
|
||||||
|
# once the image crosses ~3 GB. Strip catthehacker-resident
|
||||||
|
# toolchains we never use and any stale docker state up front.
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -76,6 +104,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||||
|
|
||||||
|
- name: Reclaim runner disk
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
df -h / || true
|
||||||
|
rm -rf \
|
||||||
|
/opt/hostedtoolcache \
|
||||||
|
/opt/microsoft \
|
||||||
|
/opt/az \
|
||||||
|
/opt/ghc \
|
||||||
|
/usr/local/.ghcup \
|
||||||
|
/usr/share/dotnet \
|
||||||
|
/usr/share/swift \
|
||||||
|
/usr/local/lib/android \
|
||||||
|
/usr/local/share/powershell \
|
||||||
|
/usr/local/share/chromium \
|
||||||
|
/usr/local/share/boost \
|
||||||
|
/usr/lib/jvm 2>/dev/null || true
|
||||||
|
apt-get clean || true
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
|
||||||
|
docker system df || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
df -h / || true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
|
|
||||||
- `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs.
|
- `Dockerfile` — single multi-stage build for both variants. OMOS variant is controlled by `INSTALL_OMOS=true` build arg; mempalace is controlled by `INSTALL_MEMPALACE` (default `true`). All GitHub-sourced binaries are pinned with version ARGs.
|
||||||
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu.
|
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu.
|
||||||
- `entrypoint-user.sh` — runs as developer: git config, opencode.json generation (delegated to `generate-config.py`), OMOS setup.
|
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), skillset auto-deploy from mounted skillset repo, OMOS setup.
|
||||||
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.json` from env vars. Never overwrites an existing config. Auto-registers MCP servers for detected tools (mempalace via the `mempalace-mcp` entry point, gitea-mcp).
|
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
|
||||||
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
||||||
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from `README.md` using explicit section rules. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
||||||
- `DOCKER_HUB.md` — **auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
- `DOCKER_HUB.md` — **auto-generated** from README. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
||||||
@@ -37,8 +37,10 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
|
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
|
||||||
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
|
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
|
||||||
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
|
||||||
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.json` — system Python can't import from the uv venv.
|
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
|
||||||
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.json`. Users bind-mount their config directory or persist it across container recreations; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
||||||
|
- **Skillset auto-deploy** — on every container start, `entrypoint-user.sh` looks for a skillset repo (detection order: `$SKILLSET_CONTAINER_PATH` → `$HOME/skillset` → `/workspace/skillset`) and runs `deploy-skills.sh --bootstrap --prune-stale`. This creates relative symlinks in `~/.agents/skills/` and `~/.config/opencode/instructions/`. Do NOT bind-mount `~/.agents/skills/` from the host — the container manages its own skills with relative symlinks that differ from the host's. The named volume `devbox-opencode-config` persists the deployed config across restarts.
|
||||||
|
- **Config persistence via named volume** — `devbox-opencode-config` is a Docker named volume mounted at `~/.config/opencode/`. It is NOT a host bind mount by default. This separation allows both native and containerized opencode to coexist on the same machine without symlink conflicts. Users who need to override can replace the named volume with a host bind mount in their compose file.
|
||||||
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
||||||
|
|
||||||
## CI quirks
|
## CI quirks
|
||||||
@@ -47,6 +49,11 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- `update-description` job runs only when both builds succeed (`needs: [build-base, build-omos]`).
|
- `update-description` job runs only when both builds succeed (`needs: [build-base, build-omos]`).
|
||||||
- Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
|
- Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
|
||||||
- Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes.
|
- Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes.
|
||||||
|
- **Gitea Actions runner has ~40 GB disk, often 70%+ used at job start.** All four `load: true` jobs (`validate-base`, `validate-omos`, `smoke-base`, `smoke-omos`) include a `Reclaim runner disk` step that strips catthehacker-resident toolchains and prunes stale docker state before `setup-buildx-action`. Build jobs use a lighter version (push-by-digest doesn't need `docker system prune`). Don't remove these steps without testing on a fresh runner.
|
||||||
|
- **`docker/build-push-action@v7` with `platforms: linux/amd64,linux/arm64` handles multi-arch push natively in a single job** — produces a proper manifest list, no matrix or merge step needed. An earlier revision split into per-arch matrix jobs with digest artifacts, but that pattern requires `actions/{upload,download}-artifact@v4+` which Gitea Actions doesn't support (see below).
|
||||||
|
- **`actions/upload-artifact` and `actions/download-artifact` must stay at @v3 on Gitea.** v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail with `GHESNotSupportedError`. If you need artifacts for a new reason (build logs, SBOMs, etc.), pin @v3 explicitly.
|
||||||
|
- **Step scripts run under `/bin/sh` (dash), not bash.** Avoid bash-isms like `${VAR//a/b}` parameter-pattern substitution; use POSIX alternatives (`tr`, `sed`) or declare `shell: bash` on the step.
|
||||||
|
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish.yml` so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.
|
||||||
|
|
||||||
## Testing changes
|
## Testing changes
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,82 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.14.40 — 2026-05-07
|
||||||
|
|
||||||
|
Bump opencode to 1.14.40.
|
||||||
|
|
||||||
|
Rolls up upstream releases v1.14.34 → v1.14.40 (no v1.14.36). Highlights:
|
||||||
|
|
||||||
|
- **v1.14.40:** support `.well-known/opencode` configs that point to a separate remote config file; assistant text preserved in signed reasoning blocks; CORS, network options, web terminal, and Cloudflare AI Gateway provider fixes; Mistral Medium 3.5 variants restored.
|
||||||
|
- **v1.14.39:** desktop app respects `HTTP_PROXY` and friends; storage reads return `null` instead of failing when keys are missing.
|
||||||
|
- **v1.14.38:** embedded UI requests work with arbitrary `connect-src` origins under the default CSP; desktop trusts system CA certificates for HTTPS.
|
||||||
|
- **v1.14.37:** cancelling a task now cancels child subtask sessions; v2 session rendering improvements (cleaner tool states, better compaction summaries); new "warp a session into another workspace or back to local project" feature; Windows titlebar stable across zoom changes.
|
||||||
|
- **v1.14.35:** preserve diff patch boundaries so session diffs render correctly when file contents themselves contain `diff --git` text.
|
||||||
|
- **v1.14.34:** PTY connection tickets for authenticated terminal websockets; v2 session failure events for clients to detect failed runs; improved shell command handling for Bash/PowerShell/cmd; new `debug info` command; `--username` option for basic-auth server connections.
|
||||||
|
|
||||||
|
No container-level changes in this release. Dockerfile bump only.
|
||||||
|
|
||||||
|
## v1.14.33 — 2026-05-03
|
||||||
|
|
||||||
|
**Bump opencode to 1.14.33. Named volume for opencode config, skillset auto-deploy, Context7 MCP.**
|
||||||
|
|
||||||
|
Rolls up the image-structure changes originally planned for v1.14.32b onto the current opencode release. v1.14.32 was built but never deployed (wrong deploy dir caught the tag mid-flight); skipped in favor of landing everything together on 1.14.33.
|
||||||
|
|
||||||
|
- **Breaking:** `~/.config/opencode/` now uses a named volume (`devbox-opencode-config`) instead of a host bind mount. The container's config, skills, and instructions are independent from the host. Users who relied on the bind mount should either re-add it explicitly in their compose file (overriding the volume) or migrate hand-edits into the container.
|
||||||
|
- **Breaking:** `~/.agents/skills/` is no longer bind-mounted from the host. The container manages its own skills directory — the entrypoint deploys skills from the skillset repo on each start.
|
||||||
|
- **Feature:** Skillset auto-deploy on container start. The entrypoint runs `deploy-skills.sh --bootstrap --prune-stale` from the first skillset repo found at: `$SKILLSET_CONTAINER_PATH` → `~/skillset` → `/workspace/skillset`. Creates relative symlinks that resolve inside the container regardless of host path layout. Idempotent.
|
||||||
|
- **Feature:** Context7 remote MCP server registered in auto-generated config. No local binary; provides up-to-date library documentation to LLMs. Config file is now `opencode.jsonc` (supports comments) with a note about the optional API key for higher rate limits. Existing-config check detects both `.json` and `.jsonc`.
|
||||||
|
- **Env:** New `SKILLSET_CONTAINER_PATH` env var for specifying skillset repo location inside the container when it's not at `/workspace/skillset`.
|
||||||
|
- **Docs:** README updated for named volume config, skillset auto-deploy, Context7 MCP server, `opencode.jsonc` references. AGENTS.md, DOCKER_HUB.md regenerated.
|
||||||
|
|
||||||
|
Upstream opencode 1.14.32 notes (shipped in this build since v1.14.32 was skipped): shell-mode input in the prompt is editable again (backspace, cursor keys); HTTP API workspace adapters no longer lose instance context, restoring workspace create/sync/routing; experimental workspace creation requests that omit `extra` are fixed; OpenAPI parameter schemas now match the public API so generated clients stop drifting; unsupported image formats fall back to text reads instead of being sent as image attachments; agents can use the global temp directory without extra permission prompts; Bedrock sessions that include reasoning content no longer break when switching models; session archive timestamps reject non-finite values to avoid invalid JSON. TUI: reduced startup theme flashing under the system theme, animated logo avoids subpixel rendering on terminals without truecolor support.
|
||||||
|
|
||||||
|
Upstream opencode 1.14.33 release notes: see https://github.com/sst/opencode/releases/tag/v1.14.33.
|
||||||
|
|
||||||
|
## v1.14.31d — 2026-05-01
|
||||||
|
|
||||||
|
**CI: collapse per-arch matrix back into single multi-arch push jobs.**
|
||||||
|
|
||||||
|
- **Fix:** `v1.14.31c`'s per-arch matrix build jobs failed on `Upload digest` with `GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES`. Gitea Actions only implements the v3-compatible artifact API; `@v4` uses a GitHub-Enterprise-specific backend. Separately, `build-omos linux/arm64` hung silently for 12 minutes in "Set-up job" and then failed with no log output — likely catthehacker image-pull contention between concurrent matrix children on the same runner host.
|
||||||
|
- Rather than downgrade to `actions/{upload,download}-artifact@v3`, collapsed the per-arch matrix entirely. `docker/build-push-action@v7` with `platforms: linux/amd64,linux/arm64` publishes a proper multi-arch manifest in a single job, so the whole artifact-passing and `imagetools create` merge dance existed only to support a matrix split we no longer need.
|
||||||
|
- The original matrix split was designed around `load: true` disk exhaustion (v1.14.30b). With `push-by-digest`/`push: true` streaming straight to the registry — no local unpack — the peak disk story is fundamentally different. Validated in v1.14.31b that the reclaim step gives sufficient headroom for a single-job amd64 build; oracle-reviewed call that this should extend to the combined amd64+arm64 push case.
|
||||||
|
- Workflow goes from 7 jobs to 5 (smoke-base, smoke-omos, build-base, build-omos, update-description). 263 → ~110 lines of YAML in `docker-publish.yml`.
|
||||||
|
- **Add:** `timeout-minutes: 90` on both build jobs so a hung arm64 build produces an explicit failure with logs rather than runner-default silent truncation.
|
||||||
|
- **Add:** `BUILDKIT_PROGRESS=plain` at workflow level so arm64-under-QEMU build output is line-by-line (the default collapsed progress UI was obscuring earlier stalls).
|
||||||
|
- **Add:** `AGENTS.md §CI quirks` documents the Gitea-specific traps encountered this week: `upload-artifact@v3`-only on Gitea, `/bin/sh` is dash, `build-push-action@v7` does multi-arch natively with comma-separated platforms, reclaim step is mandatory on `load: true` jobs.
|
||||||
|
- No image changes. Rebuild of v1.14.31 content only.
|
||||||
|
|
||||||
|
## v1.14.31c — 2026-05-01
|
||||||
|
|
||||||
|
**CI: fix bash-specific parameter expansion and bump omos size threshold.**
|
||||||
|
|
||||||
|
- **Fix:** `Derive platform slug` step in the per-arch matrix build jobs (`build-base`, `build-omos`) used `${PLATFORM_PAIR//\//-}` which is a bash parameter-expansion. The runner container executes step scripts via `/bin/sh` (dash), which errored with `Bad substitution`. Rewrote using `tr / -` which is POSIX and behaves identically. Both `build-base` and `build-omos` matrix jobs were blocked on this on `v1.14.31b`.
|
||||||
|
- **Fix:** smoke-test image-size threshold for the `omos` variant bumped from 3000 MB to 3200 MB. The mempalace-toolkit bake-in added ~100 MB to omos; measured 3107 MB on `v1.14.31b`. All functional smoke checks (opencode, node, mempalace CLIs, toolkit wrappers, oh-my-opencode-slim) pass — this is a guardrail recalibration, not a performance concession. The underlying image genuinely grew.
|
||||||
|
- The runner-disk reclaim step from v1.14.31b did its job: `smoke-base` and `validate-base` now pass cleanly. Only `smoke-omos` was blocked this iteration, and only on the threshold.
|
||||||
|
- No image changes beyond what shipped in v1.14.31. Rebuild of v1.14.31 content only.
|
||||||
|
|
||||||
|
## v1.14.31b — 2026-05-01
|
||||||
|
|
||||||
|
**CI: reclaim runner disk before `load: true` smoke builds.**
|
||||||
|
|
||||||
|
- **Fix:** v1.14.31's publish workflow and the `validate` workflow both hit `No space left on device` on the single-arch amd64 smoke/validate builds (`/opt/uv-tools/mempalace/lib/python3.13/site-packages/hf_xet/hf_xet.abi3.so`, `/usr/local/bin/git-lfs`). Root cause is not the build itself but the `load: true` step: peak disk during export equals tarball + unpacked image + buildx cache, and the image has crossed the ~3 GB threshold where this no longer fits in the ~12 GB of free space the runner container starts with. The v1.14.30c refactor split multi-arch into per-arch push-by-digest jobs (which don't `load`), but the smoke gates still do and still hit the wall.
|
||||||
|
- Added a `Reclaim runner disk` step to all four `load: true` jobs (`validate-base`, `validate-omos`, `smoke-base`, `smoke-omos`). The step strips `catthehacker/ubuntu:act-latest`-resident toolchains we never use (hosted-tool-cache, dotnet, android, powershell, swift, ghc, jvm, microsoft, chromium, boost) and runs `docker system prune -af --volumes` + `docker builder prune -af` against the runner's dockerd before `setup-buildx-action`. Expected reclaim is 6–12 GB depending on what's resident.
|
||||||
|
- Added workflow-level `concurrency: { group: ..., cancel-in-progress: false }` on `docker-publish.yml` so concurrent tag pushes can't race `docker system prune` in one job against an in-flight buildx cache in another.
|
||||||
|
- Pruning is deliberately kept out of the per-arch matrix push-by-digest jobs (`build-base`/`build-omos`) — those don't need it (no `load: true`), and pruning in parallel jobs risks one job nuking another's cache.
|
||||||
|
- **Follow-up** (not in this release): image-size reduction via a dedicated `uv tool install mempalace` build stage (strips uv's cache from the final image), pinning `mempalace-toolkit` to a commit SHA with `--depth=1 --filter=blob:none`, and auditing whether `hf_xet` is actually required by mempalace at runtime. These will ship in the next release that rebases on a new opencode version.
|
||||||
|
- No image changes. Rebuild of v1.14.31 content only.
|
||||||
|
|
||||||
|
## v1.14.31 — 2026-05-01
|
||||||
|
|
||||||
|
Bump opencode to 1.14.31.
|
||||||
|
|
||||||
|
**CI infrastructure: split multi-arch publish across separate runners.**
|
||||||
|
|
||||||
|
- **Fix:** The `publish` workflow exhausted runner disk space on `v1.14.30b` and would have hit the same wall on any subsequent release. Both variants built both architectures on a single `catthehacker/ubuntu:act-latest` container with ~40 GB of shared overlay space, and the peak disk footprint during the nodejs dpkg unpack / git-lfs layer export pushed it over the edge (`No space left on device`). The mempalace-toolkit bake-in from v1.14.30b added the final straw; the underlying issue is that QEMU-emulated arm64 layers were stored alongside the amd64 build on the same runner.
|
||||||
|
- `docker-publish.yml` refactored to the canonical `push-by-digest` + manifest-merge pattern: smoke test (amd64) runs on its own runner, each `(variant × arch)` push target runs on its own fresh runner with `outputs: type=image,...,push-by-digest=true,push=true` (no local image store), then a tiny merge job assembles the multi-arch manifest with `docker buildx imagetools create` from digest artifacts.
|
||||||
|
- Per-runner disk peak is now roughly one-quarter of the old single-job peak. The four Docker Hub tags produced per release (`vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`) are unchanged.
|
||||||
|
- Also parallelizes the amd64 and arm64 builds, so wall-clock time for a release should drop noticeably despite the added merge hop.
|
||||||
|
|
||||||
## v1.14.30b — 2026-04-30
|
## v1.14.30b — 2026-04-30
|
||||||
|
|
||||||
**Bake mempalace-toolkit wrappers into the image.**
|
**Bake mempalace-toolkit wrappers into the image.**
|
||||||
|
|||||||
+19
-61
@@ -69,9 +69,6 @@ Bind-mounted directories must exist on the host before starting the container. D
|
|||||||
```bash
|
```bash
|
||||||
# Required: workspace for your projects
|
# Required: workspace for your projects
|
||||||
mkdir -p ~/projects
|
mkdir -p ~/projects
|
||||||
|
|
||||||
# If mounting opencode config (recommended for persistent settings)
|
|
||||||
mkdir -p ~/.config/opencode
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connecting to the container
|
### Connecting to the container
|
||||||
@@ -145,28 +142,34 @@ docker compose exec -u developer devbox aws --version
|
|||||||
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
||||||
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
|
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
|
||||||
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
||||||
|
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
||||||
|
|
||||||
### Custom opencode config
|
### Custom opencode config
|
||||||
|
|
||||||
For full control over opencode settings (MCP servers, custom models, and — on the OMOS variant — oh-my-opencode-slim agents), mount the entire config directory from the host:
|
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
|
||||||
|
|
||||||
|
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
|
||||||
|
|
||||||
|
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
- ~/.config/opencode:/home/developer/.config/opencode
|
- ~/.config/opencode:/home/developer/.config/opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
This persists all configuration changes across container restarts, including `opencode.json`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json`. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped.
|
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
|
||||||
|
|
||||||
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
|
|
||||||
|
|
||||||
### Custom skills
|
### Custom skills
|
||||||
|
|
||||||
Mount agent skills from the host:
|
Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
|
||||||
|
|
||||||
```yaml
|
1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
|
||||||
volumes:
|
2. `~/skillset` mount (if present)
|
||||||
- ~/.agents/skills:/home/developer/.agents/skills:ro
|
3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
|
||||||
```
|
|
||||||
|
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
|
||||||
|
|
||||||
|
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
|
||||||
|
|
||||||
### Neovim configuration
|
### Neovim configuration
|
||||||
|
|
||||||
@@ -414,7 +417,7 @@ Without the volume, palace data lives in the container's writable layer and is l
|
|||||||
|
|
||||||
### MCP integration with opencode
|
### MCP integration with opencode
|
||||||
|
|
||||||
Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/opencode/`):
|
Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -498,7 +501,7 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
|
|||||||
GITEA_ACCESS_TOKEN=your_token_here
|
GITEA_ACCESS_TOKEN=your_token_here
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Enable the gitea MCP server in your `opencode.json`:
|
3. Enable the gitea MCP server in your `opencode.jsonc`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -516,51 +519,6 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
|
|||||||
|
|
||||||
The server is installed but disabled by default — it requires authentication to be useful.
|
The server is installed but disabled by default — it requires authentication to be useful.
|
||||||
|
|
||||||
## Shell defaults
|
|
||||||
|
|
||||||
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
|
|
||||||
|
|
||||||
Defaults you get out of the box:
|
|
||||||
|
|
||||||
- **Prefix history search** on Up/Down arrows (type `git `, press Up, walk back through prior `git ...` commands only). Ctrl-Up / Ctrl-Down still step through full history.
|
|
||||||
- **Persistent history** — `$HISTFILE` points at `~/.cache/bash/history`, backed by the `devbox-shell-history` named volume so history survives container recreation. Timestamps, 100 000 entries, dedup.
|
|
||||||
- **Case-insensitive tab completion**, coloured completion lists, `show-all-if-ambiguous`.
|
|
||||||
- **Aliases** — `ls`/`ll`/`la` use `eza`, `cat` uses `bat`, `gs`/`gd`/`gl` for git, safe `rm`/`mv`/`cp`.
|
|
||||||
- **Integrations** — `zoxide` (`z <fragment>` to jump), `fzf` Ctrl-R / Ctrl-T key bindings.
|
|
||||||
- **Prompt marker** — `[devbox]` prefix so it's always obvious you're inside the container.
|
|
||||||
|
|
||||||
### Overriding the defaults
|
|
||||||
|
|
||||||
**Option A — bind-mount host files.** Uncomment the bind-mount lines in `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- ~/.bash_aliases:/home/developer/.bash_aliases:ro
|
|
||||||
- ~/.inputrc:/home/developer/.inputrc:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Single-file bind-mount caveat (all platforms):** Docker bind-mounts the file's **inode**, not its path. When editors like vim, nvim, VS Code, or `sed -i` save a file, they write to a temp file and `rename()` it over the original — creating a new inode. The container stays pinned to the old (now unlinked) inode and never sees the update. This is a kernel limitation ([Docker #15793](https://github.com/moby/moby/issues/15793)), not fixable by Docker. Append-only writes (`echo "alias foo=bar" >> file`) are safe because they modify the same inode. **Workaround:** mount the parent directory instead of the single file (e.g. `~/.config/devbox-shell:/home/developer/.config/devbox-shell:ro`) and source files from there.
|
|
||||||
|
|
||||||
**Option B — customize inside the container.** Just edit `~/.bash_aliases` or `~/.inputrc` as normal. Pair this with a bind-mount or named volume on the home dir if you want the edits to survive container recreation.
|
|
||||||
|
|
||||||
### Restoring or diffing defaults
|
|
||||||
|
|
||||||
The skel files remain available inside every container at `/etc/skel-devbox/`. Useful commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# See what the image currently ships
|
|
||||||
cat /etc/skel-devbox/.bash_aliases
|
|
||||||
|
|
||||||
# Diff your current config against the upstream defaults
|
|
||||||
diff ~/.bash_aliases /etc/skel-devbox/.bash_aliases
|
|
||||||
|
|
||||||
# Reset to the baked defaults
|
|
||||||
cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
|
|
||||||
|
|
||||||
# …or delete the file and recreate the container — the entrypoint
|
|
||||||
# copies from /etc/skel-devbox/ on next start if the target is absent
|
|
||||||
rm ~/.bash_aliases
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -598,9 +556,9 @@ Container (Debian trixie)
|
|||||||
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
|
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
|
||||||
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
|
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
|
||||||
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
|
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
|
||||||
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
|
| `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
|
||||||
|
|
||||||
**opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above).
|
**opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ ARG DEBIAN_VERSION=trixie-slim
|
|||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG OPENCODE_VERSION=1.14.30
|
ARG OPENCODE_VERSION=1.14.40
|
||||||
|
|
||||||
LABEL maintainer="joakimp"
|
LABEL maintainer="joakimp"
|
||||||
LABEL description="Portable opencode developer container"
|
LABEL description="Portable opencode developer container"
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ Bind-mounted directories must exist on the host before starting the container. D
|
|||||||
```bash
|
```bash
|
||||||
# Required: workspace for your projects
|
# Required: workspace for your projects
|
||||||
mkdir -p ~/projects
|
mkdir -p ~/projects
|
||||||
|
|
||||||
# If mounting opencode config (recommended for persistent settings)
|
|
||||||
mkdir -p ~/.config/opencode
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connecting to the container
|
### Connecting to the container
|
||||||
@@ -125,28 +122,34 @@ docker compose exec -u developer devbox aws --version
|
|||||||
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
|
||||||
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
|
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` |
|
||||||
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` |
|
||||||
|
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
|
||||||
|
|
||||||
### Custom opencode config
|
### Custom opencode config
|
||||||
|
|
||||||
For full control over opencode settings (MCP servers, custom models, and — on the OMOS variant — oh-my-opencode-slim agents), mount the entire config directory from the host:
|
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation.
|
||||||
|
|
||||||
|
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
|
||||||
|
|
||||||
|
**Alternative: host bind-mount** — if you specifically want to share config from the host (e.g. to version-control it or sync across machines), replace the named volume with a bind mount:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
- ~/.config/opencode:/home/developer/.config/opencode
|
- ~/.config/opencode:/home/developer/.config/opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
This persists all configuration changes across container restarts, including `opencode.json`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json`. When an existing `opencode.json` is found, the `OPENCODE_PROVIDER` auto-config is skipped.
|
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.jsonc` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
|
||||||
|
|
||||||
> **Portability note:** The mounted config runs inside a Linux container. Any absolute paths inside `opencode.json` (for example, host-specific `plugin` entries like `file:///usr/local/lib/node_modules/...` or `file:///opt/homebrew/...`) will not resolve inside the container. Prefer bare package specifiers (e.g. `"oh-my-opencode-slim"`) that resolve via `node_modules` lookup, which works on both macOS and Linux hosts.
|
|
||||||
|
|
||||||
### Custom skills
|
### Custom skills
|
||||||
|
|
||||||
Mount agent skills from the host:
|
Skills are deployed automatically from a skillset repo on container start. The entrypoint detects the skillset location in this order:
|
||||||
|
|
||||||
```yaml
|
1. `SKILLSET_CONTAINER_PATH` env var (explicit path to skillset repo inside container)
|
||||||
volumes:
|
2. `~/skillset` mount (if present)
|
||||||
- ~/.agents/skills:/home/developer/.agents/skills:ro
|
3. `/workspace/skillset` fallback (if your workspace contains a `skillset/` directory)
|
||||||
```
|
|
||||||
|
When a skillset repo is detected, its skills are symlinked into `~/.agents/skills/` automatically. No manual configuration needed.
|
||||||
|
|
||||||
|
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
|
||||||
|
|
||||||
### Neovim configuration
|
### Neovim configuration
|
||||||
|
|
||||||
@@ -294,9 +297,6 @@ cd ~/<signum>/opencode-devbox
|
|||||||
cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml
|
cp /path/to/opencode-devbox/docker-compose.shared.yml docker-compose.yml
|
||||||
cp /path/to/opencode-devbox/.env.shared.example .env
|
cp /path/to/opencode-devbox/.env.shared.example .env
|
||||||
|
|
||||||
# Create per-user config directory
|
|
||||||
mkdir -p ~/<signum>/.config/opencode
|
|
||||||
|
|
||||||
# Edit .env — set SIGNUM only if you're in shared-account mode
|
# Edit .env — set SIGNUM only if you're in shared-account mode
|
||||||
vim .env
|
vim .env
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ docker compose exec -u developer devbox opencode
|
|||||||
Each user's container, config, and named volumes are fully isolated:
|
Each user's container, config, and named volumes are fully isolated:
|
||||||
- Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode)
|
- Container name: `devbox-<signum>` (or `devbox-$USER` in own-account mode)
|
||||||
- Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for isolation
|
- Named volumes: prefixed with the project name (`devbox-<signum>_devbox-data`, etc.) — the Docker daemon is system-wide, so directory-name prefixing alone is NOT sufficient for isolation
|
||||||
- Opencode config: `~/<signum>/.config/opencode/` (per-user settings, OMOS config, etc.)
|
- Opencode config: persisted via per-user named volume (`devbox-<signum>_devbox-opencode-config`)
|
||||||
|
|
||||||
See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration.
|
See `docker-compose.shared.yml` and `.env.shared.example` for the full configuration.
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ Without the volume, palace data lives in the container's writable layer and is l
|
|||||||
|
|
||||||
### MCP integration with opencode
|
### MCP integration with opencode
|
||||||
|
|
||||||
Add mempalace as an MCP server in your `opencode.json` (inside `~/.config/opencode/`):
|
Add mempalace as an MCP server in your `opencode.jsonc` (inside `~/.config/opencode/`):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -552,7 +552,7 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
|
|||||||
GITEA_ACCESS_TOKEN=your_token_here
|
GITEA_ACCESS_TOKEN=your_token_here
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Enable the gitea MCP server in your `opencode.json`:
|
3. Enable the gitea MCP server in your `opencode.jsonc`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcp": {
|
"mcp": {
|
||||||
@@ -570,6 +570,14 @@ The image includes the [official Gitea MCP server](https://gitea.com/gitea/gitea
|
|||||||
|
|
||||||
The server is installed but disabled by default — it requires authentication to be useful.
|
The server is installed but disabled by default — it requires authentication to be useful.
|
||||||
|
|
||||||
|
## Context7 MCP server
|
||||||
|
|
||||||
|
The image auto-registers a [Context7](https://context7.com) MCP server, which provides up-to-date library documentation and code examples to LLMs at query time. This is a remote MCP server at `mcp.context7.com/mcp` — no local binary is needed.
|
||||||
|
|
||||||
|
- Auto-registered in the generated `opencode.jsonc` (no manual setup required)
|
||||||
|
- Provides documentation for any programming library/framework on demand
|
||||||
|
- Requires internet access — useless in air-gapped/offline environments
|
||||||
|
|
||||||
## Shell defaults
|
## Shell defaults
|
||||||
|
|
||||||
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
|
The image ships a baked `.bash_aliases` and `.inputrc` with quality-of-life defaults. On first container start they are copied from `/etc/skel-devbox/` into `/home/developer/` **only if the target file does not already exist** — so host bind-mounts and any version you've customized inside the container are never overwritten on upgrade.
|
||||||
@@ -680,9 +688,9 @@ Container (Debian trixie)
|
|||||||
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
|
| `/home/developer/.rustup` | Named volume `devbox-rustup` (if configured) | ✅ Yes | Rust toolchains |
|
||||||
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
|
| `/home/developer/.cargo` | Named volume `devbox-cargo` (if configured) | ✅ Yes | Cargo binaries, registry cache |
|
||||||
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
|
| `/home/developer/.vscode-server` | Named volume `devbox-vscode` (if configured) | ✅ Yes | VS Code server and extensions |
|
||||||
| `/home/developer/.config/opencode` | Host bind mount (if configured) | ✅ Yes | opencode.json, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
|
| `/home/developer/.config/opencode` | Named volume `devbox-opencode-config` | ✅ Yes | `opencode.jsonc`, skills, plus `oh-my-opencode-slim.json` on the OMOS variant |
|
||||||
|
|
||||||
**opencode config** (`opencode.json`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, mount the config directory from the host (see Custom opencode config above).
|
**opencode config** (`opencode.jsonc`) is auto-generated from `OPENCODE_PROVIDER` on each start. It sets provider and model only — no MCP servers. To persist config changes and use custom settings, use the named volume (default) or bind-mount from host (see Custom opencode config above).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,18 @@ services:
|
|||||||
# SSH keys — user-specific if available, else shared
|
# SSH keys — user-specific if available, else shared
|
||||||
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
|
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
|
||||||
|
|
||||||
# Opencode config — per-user (persists settings across restarts)
|
# Optional: mount skillset repo for automatic skill/instruction deployment.
|
||||||
- ${HOME}/${SIGNUM}/.config/opencode:/home/developer/.config/opencode
|
# The entrypoint runs deploy-skills.sh --bootstrap on start, creating
|
||||||
|
# relative symlinks that resolve inside the container regardless of
|
||||||
|
# where the repo lives on the host. Set SKILLSET_PATH in .env.
|
||||||
|
# - ${SKILLSET_PATH}:/home/developer/skillset
|
||||||
|
|
||||||
|
# Persist opencode config (opencode.jsonc, oh-my-opencode-slim.json,
|
||||||
|
# instructions, etc.) across container recreations. Auto-generated on
|
||||||
|
# first start from env vars by generate-config.py and the skillset
|
||||||
|
# deploy script. Using a named volume keeps the container's symlinks
|
||||||
|
# independent from the host.
|
||||||
|
- devbox-opencode-config:/home/developer/.config/opencode
|
||||||
|
|
||||||
# Persist opencode data (auth, memory, session history)
|
# Persist opencode data (auth, memory, session history)
|
||||||
- devbox-data:/home/developer/.local/share/opencode
|
- devbox-data:/home/developer/.local/share/opencode
|
||||||
@@ -73,6 +83,7 @@ services:
|
|||||||
# - ${HOME}/${SIGNUM}/.aws:/home/developer/.aws
|
# - ${HOME}/${SIGNUM}/.aws:/home/developer/.aws
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
devbox-opencode-config:
|
||||||
devbox-data:
|
devbox-data:
|
||||||
devbox-shell-history:
|
devbox-shell-history:
|
||||||
devbox-zoxide:
|
devbox-zoxide:
|
||||||
|
|||||||
+19
-6
@@ -42,13 +42,25 @@ services:
|
|||||||
# SSH keys (read-only) — for git push/pull
|
# SSH keys (read-only) — for git push/pull
|
||||||
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
|
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
|
||||||
|
|
||||||
# Optional: mount opencode config directory (persists config changes across restarts)
|
# Optional: mount skillset repo for automatic skill/instruction deployment.
|
||||||
# Includes opencode.json, oh-my-opencode-slim.json, skills, etc.
|
# The entrypoint runs deploy-skills.sh --bootstrap on start, creating
|
||||||
# When mounted, OPENCODE_PROVIDER auto-config is skipped if opencode.json exists.
|
# relative symlinks that resolve inside the container regardless of
|
||||||
# - ~/.config/opencode:/home/developer/.config/opencode
|
# where the repo lives on the host. Set SKILLSET_PATH in .env.
|
||||||
|
# - ${SKILLSET_PATH}:/home/developer/skillset
|
||||||
|
|
||||||
# Optional: mount opencode agent skills from host
|
# Persist opencode config (opencode.jsonc, oh-my-opencode-slim.json,
|
||||||
# - ~/.agents/skills:/home/developer/.agents/skills:ro
|
# instructions, etc.) across container recreations. Auto-generated on
|
||||||
|
# first start from env vars by generate-config.py and the skillset
|
||||||
|
# deploy script. Using a named volume (not a host bind mount) keeps
|
||||||
|
# the container's skill/instruction symlinks independent from the host,
|
||||||
|
# allowing both native and containerized opencode on the same machine.
|
||||||
|
- devbox-opencode-config:/home/developer/.config/opencode
|
||||||
|
|
||||||
|
# NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The
|
||||||
|
# container manages its own skills directory independently — the
|
||||||
|
# entrypoint deploys skills from the skillset repo on each start.
|
||||||
|
# Sharing it with the host causes symlink conflicts (relative paths
|
||||||
|
# differ between host and container filesystem namespaces).
|
||||||
|
|
||||||
# Optional: mount neovim config from host (plugins auto-install on first start)
|
# Optional: mount neovim config from host (plugins auto-install on first start)
|
||||||
# - ~/.config/nvim:/home/developer/.config/nvim:ro
|
# - ~/.config/nvim:/home/developer/.config/nvim:ro
|
||||||
@@ -108,6 +120,7 @@ services:
|
|||||||
# - ~/.aws:/home/developer/.aws
|
# - ~/.aws:/home/developer/.aws
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
devbox-opencode-config:
|
||||||
devbox-data:
|
devbox-data:
|
||||||
devbox-state:
|
devbox-state:
|
||||||
devbox-shell-history:
|
devbox-shell-history:
|
||||||
|
|||||||
@@ -44,6 +44,28 @@ fi
|
|||||||
# generated) and no-ops if OPENCODE_PROVIDER is unset.
|
# generated) and no-ops if OPENCODE_PROVIDER is unset.
|
||||||
python3 /usr/local/lib/opencode-devbox/generate-config.py
|
python3 /usr/local/lib/opencode-devbox/generate-config.py
|
||||||
|
|
||||||
|
# ── Skillset: deploy skills/instructions from mounted skillset repo ──
|
||||||
|
# When the skillset repo is mounted (at $HOME/skillset or /workspace/skillset),
|
||||||
|
# run the deploy script to create relative symlinks for skills and instructions.
|
||||||
|
# This ensures skills resolve correctly inside the container regardless of
|
||||||
|
# where the repo lives on the host. Idempotent — second run is a no-op.
|
||||||
|
#
|
||||||
|
# Detection order:
|
||||||
|
# 1. SKILLSET_CONTAINER_PATH env var (explicit, for non-standard layouts)
|
||||||
|
# 2. $HOME/skillset (dedicated volume mount via SKILLSET_PATH in compose)
|
||||||
|
# 3. /workspace/skillset (skillset is directly inside workspace root)
|
||||||
|
SKILLSET_DEPLOY=""
|
||||||
|
if [ -n "${SKILLSET_CONTAINER_PATH:-}" ] && [ -x "${SKILLSET_CONTAINER_PATH}/deploy-skills.sh" ]; then
|
||||||
|
SKILLSET_DEPLOY="${SKILLSET_CONTAINER_PATH}/deploy-skills.sh"
|
||||||
|
elif [ -x "$HOME/skillset/deploy-skills.sh" ]; then
|
||||||
|
SKILLSET_DEPLOY="$HOME/skillset/deploy-skills.sh"
|
||||||
|
elif [ -x /workspace/skillset/deploy-skills.sh ]; then
|
||||||
|
SKILLSET_DEPLOY="/workspace/skillset/deploy-skills.sh"
|
||||||
|
fi
|
||||||
|
if [ -n "$SKILLSET_DEPLOY" ]; then
|
||||||
|
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
CONFIG_DIR="$HOME/.config/opencode"
|
CONFIG_DIR="$HOME/.config/opencode"
|
||||||
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ def register_mcp_servers(config: dict) -> list[str]:
|
|||||||
"enabled": False,
|
"enabled": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Context7 — up-to-date library documentation for LLMs (remote).
|
||||||
|
# Free tier works without an API key; set CONTEXT7_API_KEY for higher
|
||||||
|
# rate limits. No local binary needed — purely a remote MCP endpoint.
|
||||||
|
servers["context7"] = {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://mcp.context7.com/mcp",
|
||||||
|
}
|
||||||
|
|
||||||
if servers:
|
if servers:
|
||||||
config["mcp"] = servers
|
config["mcp"] = servers
|
||||||
|
|
||||||
@@ -110,14 +118,17 @@ def main() -> int:
|
|||||||
|
|
||||||
home = Path(os.environ.get("HOME", "/home/developer"))
|
home = Path(os.environ.get("HOME", "/home/developer"))
|
||||||
config_dir = home / ".config" / "opencode"
|
config_dir = home / ".config" / "opencode"
|
||||||
config_file = config_dir / "opencode.json"
|
config_file = config_dir / "opencode.jsonc"
|
||||||
|
config_file_legacy = config_dir / "opencode.json"
|
||||||
|
|
||||||
# CRITICAL: never overwrite an existing config. Users may have
|
# CRITICAL: never overwrite an existing config. Users may have
|
||||||
# bind-mounted their host config directory, or their config may be
|
# bind-mounted their host config directory, or their config may be
|
||||||
# persisted in a named volume from a previous run.
|
# persisted in a named volume from a previous run.
|
||||||
if config_file.exists():
|
# Check both .json and .jsonc variants.
|
||||||
|
if config_file.exists() or config_file_legacy.exists():
|
||||||
|
existing = config_file if config_file.exists() else config_file_legacy
|
||||||
print(
|
print(
|
||||||
f"Existing opencode.json found at {config_file} — "
|
f"Existing config found at {existing} — "
|
||||||
"skipping generation.",
|
"skipping generation.",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
@@ -140,8 +151,23 @@ def main() -> int:
|
|||||||
added = register_mcp_servers(config)
|
added = register_mcp_servers(config)
|
||||||
|
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Write as JSONC so we can include helpful comments.
|
||||||
|
content = json.dumps(config, indent=2)
|
||||||
|
|
||||||
|
# Insert a comment about Context7 API key after the context7 url line.
|
||||||
|
context7_comment = (
|
||||||
|
' "url": "https://mcp.context7.com/mcp"\n'
|
||||||
|
" // For higher rate limits, sign up at https://context7.com/dashboard\n"
|
||||||
|
' // and add: "headers": { "CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}" }'
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
' "url": "https://mcp.context7.com/mcp"',
|
||||||
|
context7_comment,
|
||||||
|
)
|
||||||
|
|
||||||
with config_file.open("w") as f:
|
with config_file.open("w") as f:
|
||||||
json.dump(config, f, indent=2)
|
f.write(content)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
if added:
|
if added:
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ SECTION_RULES: dict[str, str] = {
|
|||||||
"AWS Bedrock Authentication": "keep",
|
"AWS Bedrock Authentication": "keep",
|
||||||
"MemPalace — persistent AI memory": "keep",
|
"MemPalace — persistent AI memory": "keep",
|
||||||
"Gitea MCP server": "keep",
|
"Gitea MCP server": "keep",
|
||||||
"Shell defaults": "keep",
|
"Context7 MCP server": "drop",
|
||||||
|
"Shell defaults": "drop", # detail, full README covers it
|
||||||
"Secret Scanning": "drop", # dev-only — gitleaks is for committers
|
"Secret Scanning": "drop", # dev-only — gitleaks is for committers
|
||||||
"Architecture": "keep",
|
"Architecture": "keep",
|
||||||
"License": "replace", # point at source repo instead
|
"License": "replace", # point at source repo instead
|
||||||
|
|||||||
+21
-11
@@ -160,11 +160,11 @@ else
|
|||||||
fi
|
fi
|
||||||
rm -f "$tmpout"
|
rm -f "$tmpout"
|
||||||
|
|
||||||
# Config generation with anthropic provider writes valid JSON with the
|
# Config generation with anthropic provider writes valid JSONC with the
|
||||||
# expected shape. The script's log message goes to stderr (line 1 of
|
# expected shape. The script's log message goes to stderr (line 1 of
|
||||||
# generate-config.py uses file=sys.stderr) so capturing only stdout
|
# generate-config.py uses file=sys.stderr) so capturing only stdout
|
||||||
# gives us clean JSON.
|
# gives us clean JSONC. We strip // comments before validating JSON.
|
||||||
label="generate-config produces valid opencode.json"
|
label="generate-config produces valid opencode.jsonc"
|
||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
if docker run --rm \
|
if docker run --rm \
|
||||||
-e OPENCODE_PROVIDER=anthropic \
|
-e OPENCODE_PROVIDER=anthropic \
|
||||||
@@ -173,24 +173,31 @@ if docker run --rm \
|
|||||||
"$IMAGE" sh -c '
|
"$IMAGE" sh -c '
|
||||||
mkdir -p /tmp/home
|
mkdir -p /tmp/home
|
||||||
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
||||||
cat /tmp/home/.config/opencode/opencode.json
|
cat /tmp/home/.config/opencode/opencode.jsonc
|
||||||
' > "$tmp/out.json" 2>/dev/null; then
|
' > "$tmp/out.jsonc" 2>/dev/null; then
|
||||||
|
# Strip single-line // comments for JSON validation (respecting strings)
|
||||||
if python3 -c "
|
if python3 -c "
|
||||||
import json, sys
|
import re, json, sys
|
||||||
c = json.load(open('$tmp/out.json'))
|
text = open('$tmp/out.jsonc').read()
|
||||||
|
# Match either a string literal or a // comment; keep strings, drop comments
|
||||||
|
pattern = r'\"(?:\\\\.|[^\"\\\\])*\"|//[^\n]*'
|
||||||
|
stripped = re.sub(pattern, lambda m: m.group(0) if m.group(0).startswith('\"') else '', text)
|
||||||
|
c = json.loads(stripped)
|
||||||
assert c['model'].startswith('anthropic/'), c
|
assert c['model'].startswith('anthropic/'), c
|
||||||
assert c['autoupdate'] is False
|
assert c['autoupdate'] is False
|
||||||
assert c['share'] == 'disabled'
|
assert c['share'] == 'disabled'
|
||||||
|
assert 'context7' in c.get('mcp', {}), 'context7 MCP not registered'
|
||||||
" 2>&1; then
|
" 2>&1; then
|
||||||
pass "$label"
|
pass "$label"
|
||||||
else
|
else
|
||||||
fail "$label: output doesn't match expected shape: $(cat "$tmp/out.json")"
|
fail "$label: output doesn't match expected shape: $(cat "$tmp/out.jsonc")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
fail "$label: container failed: $(cat "$tmp/out.json")"
|
fail "$label: container failed: $(cat "$tmp/out.jsonc")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Config generation is idempotent — running twice must not overwrite.
|
# Config generation is idempotent — running twice must not overwrite.
|
||||||
|
# Tests both legacy .json and new .jsonc detection.
|
||||||
label="generate-config never overwrites existing config"
|
label="generate-config never overwrites existing config"
|
||||||
if docker run --rm \
|
if docker run --rm \
|
||||||
-e OPENCODE_PROVIDER=anthropic \
|
-e OPENCODE_PROVIDER=anthropic \
|
||||||
@@ -214,9 +221,12 @@ SIZE_BYTES=$(docker image inspect --format='{{.Size}}' "$IMAGE")
|
|||||||
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
|
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
|
||||||
echo " Uncompressed size: ${SIZE_MB} MB"
|
echo " Uncompressed size: ${SIZE_MB} MB"
|
||||||
|
|
||||||
# Thresholds (uncompressed): base 2500 MB, omos 3000 MB. Adjust as image content evolves.
|
# Thresholds (uncompressed): base 2500 MB, omos 3200 MB. Adjust as image content evolves.
|
||||||
|
# omos bumped 3000→3200 on v1.14.31c — mempalace-toolkit bake-in pushed the
|
||||||
|
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
|
||||||
|
# guardrail, not a performance limit.
|
||||||
THRESHOLD=2500
|
THRESHOLD=2500
|
||||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3000
|
[ "$VARIANT" = "omos" ] && THRESHOLD=3200
|
||||||
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
||||||
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user