Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,32 +110,70 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# Smoke test: build the amd64 image and verify the pinned OpenCode
# version runs before anything is published.
# Smoke test: build each platform image and verify the pinned OpenCode
# version runs before anything is published. arm64 runs under QEMU;
# both builds warm the cache for the multi-arch push below.
- name: Build amd64 image for smoke test
if: steps.changes.outputs.build == 'true'
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
platforms: linux/amd64
load: true
tags: sbx-opencode2:smoke
tags: sbx-opencode2:smoke-amd64
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Build arm64 image for smoke test
if: steps.changes.outputs.build == 'true'
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
platforms: linux/arm64
load: true
tags: sbx-opencode2:smoke-arm64
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke test (opencode --version)
if: steps.changes.outputs.build == 'true'
run: |
EXPECTED=$(sed -n 's/^ARG OPENCODE_VERSION=//p' Dockerfile)
ACTUAL=$(docker run --rm sbx-opencode2:smoke opencode --version)
echo "expected=v${EXPECTED} actual=${ACTUAL}"
echo "${ACTUAL}" | grep -q "v${EXPECTED}"
for arch in amd64 arm64; do
ACTUAL=$(docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" opencode --version)
echo "${arch}: expected=v${EXPECTED} actual=${ACTUAL}"
echo "${ACTUAL}" | grep -q "v${EXPECTED}"
done

# The npm prefix is agent-owned in the base image; installing as root
# would leave the package root-owned and break in-sandbox self-updates.
- name: Smoke test (package writable by agent)
if: steps.changes.outputs.build == 'true'
run: docker run --rm sbx-opencode2:smoke sh -c 'test -w "$NPM_CONFIG_PREFIX/lib/node_modules/@opencode/cli"'
run: |
for arch in amd64 arm64; do
docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" sh -c 'test -w "$NPM_CONFIG_PREFIX/lib/node_modules/@opencode/cli"'
done

# The package nests a ~200 MB binary per libc variant; the Dockerfile
# drops them after the postinstall. Fail if they come back, e.g. after
# an upstream layout change makes the rm miss them. One binary is
# ~200 MB, so 300 MB leaves headroom without hiding a second one.
- name: Smoke test (no nested platform binaries)
if: steps.changes.outputs.build == 'true'
run: |
for arch in amd64 arm64; do
docker run --rm --platform "linux/${arch}" "sbx-opencode2:smoke-${arch}" sh -euc '
pkg="$NPM_CONFIG_PREFIX/lib/node_modules/@opencode"
for dir in "$pkg"/cli/node_modules/@opencode/cli-* "$pkg"/cli-*; do
if [ -e "$dir" ]; then
echo "platform package present: $dir"; exit 1
fi
done
size=$(du -sm "$pkg" | cut -f1)
echo "'"${arch}"': @opencode is ${size} MB"
test "$size" -lt 300
'
done

# Tag published images with the pinned OpenCode version, so an
# automerged Renovate bump releases itself without a manual git tag.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## CI: build and publish

`.github/workflows/publish.yml` runs on every PR and every push to `main`. It detects build-relevant changes (`Dockerfile`, `.dockerignore`, the workflow itself), comparing push endpoints with a two-dot diff and PRs against the merge base with a three-dot diff. Relevant changes trigger an amd64 smoke test — the OpenCode version must match the `Dockerfile` and the npm prefix must be writable by `agent` — and a multi-arch (amd64 + arm64) build. PRs build and test without publishing. Only relevant pushes to `main` and manual runs on `main` publish to GHCR and Docker Hub with provenance attestations, tagged with `OPENCODE_VERSION` from the `Dockerfile` and `latest`.
`.github/workflows/publish.yml` runs on every PR and every push to `main`. It detects build-relevant changes (`Dockerfile`, `.dockerignore`, the workflow itself), comparing push endpoints with a two-dot diff and PRs against the merge base with a three-dot diff. Relevant changes trigger amd64 and arm64 smoke tests (arm64 under QEMU) — the OpenCode version must match the `Dockerfile`, the npm prefix must be writable by `agent`, and no nested platform binaries may remain — and a multi-arch (amd64 + arm64) build. PRs build and test without publishing. Only relevant pushes to `main` and manual runs on `main` publish to GHCR and Docker Hub with provenance attestations, tagged with `OPENCODE_VERSION` from the `Dockerfile` and `latest`.

Docs-only changes skip the build but the required `build-and-push` job still succeeds. Manual runs (`workflow_dispatch`) always build; runs on other branches do not publish. If the comparison base is unavailable (new branch, missing commit), the build runs conservatively. Actual diff errors fail the job rather than reporting a successful skip.

Expand Down
23 changes: 17 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Docker Sandboxes template: pinned OpenCode v2 on the opencode-docker variant.
# Docker Sandboxes template: pinned OpenCode v2 on the shell-docker variant.
# Build: docker build -t sbx-opencode2:local .
# Bump: Renovate opens PRs for the base image digest and OPENCODE_VERSION;
# merging to main publishes the version-tagged image.

# shell-docker is opencode-docker without the OpenCode V1 layer (~520 MB
# compressed). An npm uninstall on top of opencode-docker would only add
# whiteouts; the V1 layer would still ship in the image.
# renovate: datasource=docker depName=docker/sandbox-templates
FROM docker/sandbox-templates:opencode-docker@sha256:ef636291701f88bbe1a268bd083175804a84309c14faf75bc8c2ed179ce61db3
FROM docker/sandbox-templates:shell-docker@sha256:1560168ac5fb9ce23d413c878349334c5845c07e264cd675d7867f0c78ad1761

# renovate: datasource=npm depName=@opencode/cli
ARG OPENCODE_VERSION=2.0.32
Expand All @@ -13,13 +16,21 @@ LABEL org.opencontainers.image.source=https://github.com/nikbucher/sbx-opencode2
LABEL org.opencontainers.image.description="Docker Sandboxes template with a pinned OpenCode v2 release"
LABEL org.opencontainers.image.licenses=MIT

# The base image ships OpenCode V1 (npm package "opencode-ai").
# Replace it with a pinned V2 release from the @opencode/cli package.
# Install a pinned V2 release from the @opencode/cli package.
# Run the install as agent, not root: the base image's NPM_CONFIG_PREFIX
# is agent-owned, and agent-owned package files are what make in-sandbox
# self-updates (/update, opencode upgrade) work at all.
# The platform binaries are optionalDependencies without a libc field, so
# npm installs both the glibc and musl variant (~200 MB each). The package
# has no other dependencies, and the postinstall hard-links (or copies) the
# matching binary into bin/, so its nested node_modules can go entirely.
# The empty config dir matches opencode-docker, which ships it agent-owned.
USER agent
RUN npm uninstall -g opencode-ai \
&& npm install -g "@opencode/cli@${OPENCODE_VERSION}" \
RUN npm install -g "@opencode/cli@${OPENCODE_VERSION}" \
&& rm -rf "$(npm root -g)/@opencode/cli/node_modules" \
&& npm cache clean --force \
&& mkdir -p "$HOME/.config/opencode" \
&& opencode --version

# shell-docker defaults to bash; restore opencode-docker's default command.
CMD ["opencode"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Run OpenCode v2 in [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/customize/templates/) with a consistent starting version, on amd64 and arm64.

The base template ships OpenCode V1; this image swaps in the pinned V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag.
The image builds on the `shell-docker` sandbox template and adds the pinned OpenCode V2 CLI, so every sandbox starts with the exact same OpenCode release. `/update` inside a running sandbox upgrades only that sandbox — the durable path to a newer OpenCode is a newer image tag. The image keeps only the binary for its platform; an in-sandbox update reinstalls the unused libc variant too, adding ~200 MB to that sandbox.

## Use it

Expand Down
Loading