Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f2c9f5112 | |||
| 60eb49469e | |||
| 18b9c9c549 | |||
| ad4a12b3ab | |||
| fde5a89e8b | |||
| 034830710c | |||
| d293ddc202 | |||
| 910378fe06 | |||
| f06a70a3bc | |||
| dba05da7d1 | |||
| 8359fef949 | |||
| a438c67f06 | |||
| 07e07ec611 | |||
| 7dc836ab66 | |||
| a3ff601bf0 | |||
| 6fde27c212 | |||
| b30ffc83bd | |||
| 896380bb9c | |||
| 911d6dd26b |
@@ -0,0 +1,282 @@
|
|||||||
|
# CI / Build Pipeline
|
||||||
|
|
||||||
|
This directory contains the gitea Actions workflows and the supporting
|
||||||
|
documentation for opencode-devbox's CI. If you're investigating *why*
|
||||||
|
the build pipeline is shaped the way it is, you're in the right place.
|
||||||
|
|
||||||
|
## Workflows in this directory
|
||||||
|
|
||||||
|
| File | Trigger | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| [`workflows/docker-publish-split.yml`](workflows/docker-publish-split.yml) | `push: tags: v*` | **Production release pipeline.** Two-phase split-base build: shared `base-<hash>` published once (skipped on cache hit), then four parallel variant deltas. ~40–80 min wall clock depending on runner count and whether base needs rebuilding. |
|
||||||
|
| [`workflows/validate.yml`](workflows/validate.yml) | `push: branches: main` + PR | **Lightweight gate.** amd64-only smoke test of all four variants + `DOCKER_HUB.md` sync check. ~30 min. Fires on every push to `main`. |
|
||||||
|
|
||||||
|
## Why the split-base pipeline exists
|
||||||
|
|
||||||
|
opencode-devbox publishes **four image variants** (`base`, `omos`, `with-pi`, `omos-with-pi`) × **two architectures** (amd64, arm64) = **eight image tags per release**. Today's runners are 2 self-hosted gitea Actions runners. arm64 builds are emulated under QEMU, which is the dominant cost (~3–5x slower than native).
|
||||||
|
|
||||||
|
The four variants share ~95% of their layers (Debian + apt + Node + AWS CLI + mempalace + dev tools + entrypoints). The original `Dockerfile` was a single multi-stage build with `INSTALL_*` build-args gating variant-specific RUNs. BuildKit's per-layer cache key is content-addressed, but as soon as a build-arg-gated `RUN` produces a different layer hash for variant A vs variant B, every subsequent layer also has a different parent → identical commands re-execute per variant. Result: minimal cross-variant cache reuse on a fresh build.
|
||||||
|
|
||||||
|
Two improvements were considered:
|
||||||
|
|
||||||
|
1. **Reorder the original Dockerfile** so all variant-gated RUNs land at the bottom — modest gain, ~10–20% wall-clock reduction. *Not pursued.*
|
||||||
|
2. **Split into `Dockerfile.base` + `Dockerfile.variant`** with the base published as a long-lived shared image — significant gain, ~50–70% wall-clock reduction with hash-driven cache reuse. *Pursued.*
|
||||||
|
|
||||||
|
The split-base architecture is what the `docker-publish-split.yml` workflow exercises.
|
||||||
|
|
||||||
|
## How the split-base pipeline works
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
│ base-decide │ compute base-<hash>;
|
||||||
|
│ │ probe Docker Hub.
|
||||||
|
│ hash inputs: │
|
||||||
|
│ Dockerfile.base│
|
||||||
|
│ rootfs/ │
|
||||||
|
│ entrypoint*.sh │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌─────────────┴─────────────┐
|
||||||
|
│ need_build = true? │
|
||||||
|
└─────────────┬─────────────┘
|
||||||
|
yes │ no
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ build-base │ multi-arch build,
|
||||||
|
│ │ push base-<hash>
|
||||||
|
└────────┬─────────┘ to Docker Hub.
|
||||||
|
│
|
||||||
|
┌───────────────────────┼───────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
||||||
|
│smoke-base│ │smoke-omos│ ... │smoke-omos-pi │ amd64 only,
|
||||||
|
└────┬─────┘ └────┬─────┘ └──────┬───────┘ parallel.
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
||||||
|
│build- │ │build- │ │build- │ multi-arch,
|
||||||
|
│variant- │ │variant- │ ... │variant- │ parallel,
|
||||||
|
│base │ │omos │ │omos-with-pi │ tag push.
|
||||||
|
└────┬─────┘ └────┬─────┘ └──────┬───────┘
|
||||||
|
└───────────────────────┴──────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ promote-base-latest │ crane copy
|
||||||
|
│ │ base-<hash>
|
||||||
|
│ │ → base-latest
|
||||||
|
└────────┬─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ update-description │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1: `base-decide`
|
||||||
|
|
||||||
|
Compute a SHA-256 hash over the inputs that determine the base image's
|
||||||
|
content:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
{
|
||||||
|
cat Dockerfile.base
|
||||||
|
find rootfs -type f -print0 | sort -z | xargs -0 cat
|
||||||
|
cat entrypoint.sh entrypoint-user.sh
|
||||||
|
} | sha256sum | cut -c1-12
|
||||||
|
```
|
||||||
|
|
||||||
|
The 12-character truncated hash becomes `base-<hash>`. Probe Docker Hub
|
||||||
|
for this tag via `docker manifest inspect`:
|
||||||
|
|
||||||
|
- If it exists → set `need_build=false`. `build-base` is skipped entirely.
|
||||||
|
- If it doesn't → set `need_build=true`. `build-base` runs.
|
||||||
|
|
||||||
|
This is the core cache-reuse mechanism. Version-bump-only releases
|
||||||
|
(only `Dockerfile.variant` or build-args changed) hit the cache. Releases
|
||||||
|
that change anything in the base — apt packages, AWS CLI, Node version,
|
||||||
|
locale list, entrypoint scripts — pay the full base-build cost once.
|
||||||
|
|
||||||
|
### Step 2: `build-base` (conditional)
|
||||||
|
|
||||||
|
Only runs when `need_build=true`. Multi-arch (amd64 + arm64) build of
|
||||||
|
`Dockerfile.base`, pushed to `joakimp/opencode-devbox:base-<hash>`.
|
||||||
|
Registry cache via `--cache-from/--cache-to` reduces incremental rebuilds
|
||||||
|
when only one or two layers changed.
|
||||||
|
|
||||||
|
The base image is **not** tagged `base-latest` here — that promotion
|
||||||
|
happens at the very end after all variants succeed (see step 5).
|
||||||
|
|
||||||
|
### Step 3: `smoke-*` (×4, parallel)
|
||||||
|
|
||||||
|
For each variant: build amd64-only against the base tag, load into
|
||||||
|
local docker, run [`scripts/smoke-test.sh`](../scripts/smoke-test.sh).
|
||||||
|
Variant build-args:
|
||||||
|
|
||||||
|
| variant | INSTALL_OPENCODE | INSTALL_OMOS | INSTALL_PI |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `base` | true | false | false |
|
||||||
|
| `omos` | true | true | false |
|
||||||
|
| `with-pi` | true | false | true |
|
||||||
|
| `omos-with-pi` | true | true | true |
|
||||||
|
|
||||||
|
Smoke runs `--variant <name>` to enable variant-specific assertions.
|
||||||
|
Gate the publish: a smoke failure for variant X blocks `build-variant-X`.
|
||||||
|
|
||||||
|
### Step 4: `build-variant-*` (×4, parallel)
|
||||||
|
|
||||||
|
For each variant that passed smoke: multi-arch (amd64 + arm64) build of
|
||||||
|
`Dockerfile.variant`, pushed to Docker Hub with the user-facing release
|
||||||
|
tags:
|
||||||
|
|
||||||
|
| Build job | Tags pushed |
|
||||||
|
|---|---|
|
||||||
|
| `build-variant-base` | `vX.Y.Z`, `latest` |
|
||||||
|
| `build-variant-omos` | `vX.Y.Z-omos`, `latest-omos` |
|
||||||
|
| `build-variant-with-pi` | `vX.Y.Z-with-pi`, `latest-with-pi` |
|
||||||
|
| `build-variant-omos-with-pi` | `vX.Y.Z-omos-with-pi`, `latest-omos-with-pi` |
|
||||||
|
|
||||||
|
The `latest*` aliases are only updated when `promote_latest=true` (the
|
||||||
|
manual dispatch input) — for test runs, `promote_latest=false` keeps the
|
||||||
|
production aliases pointing at the previous good release.
|
||||||
|
|
||||||
|
### Step 5: `promote-base-latest`
|
||||||
|
|
||||||
|
Once all four variants successfully publish, re-tag `base-<hash>` as
|
||||||
|
`base-latest` using `crane copy`. This is a **manifest-level re-tag, not
|
||||||
|
a rebuild** — it touches only Docker Hub's image index, takes seconds,
|
||||||
|
and is atomic.
|
||||||
|
|
||||||
|
The reason this happens *after* variants succeed (rather than alongside
|
||||||
|
`build-base`) is so a partial failure leaves `base-latest` pointing at
|
||||||
|
the previous known-good base. External consumers who pin to
|
||||||
|
`base-latest` (e.g. the planned pi-devbox repo) never see a broken base.
|
||||||
|
|
||||||
|
### Step 6: `update-description`
|
||||||
|
|
||||||
|
Push the generated `DOCKER_HUB.md` to the Hub repo's `full_description`
|
||||||
|
field via the Hub REST API. Same step as the production pipeline.
|
||||||
|
|
||||||
|
## NPM_CONFIG_PREFIX gotcha (variant override pattern)
|
||||||
|
|
||||||
|
The base sets
|
||||||
|
|
||||||
|
```
|
||||||
|
ENV NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global
|
||||||
|
```
|
||||||
|
|
||||||
|
This is intentional — it makes `pi install npm:<pkg>` and `npm install -g`
|
||||||
|
land on the `devbox-pi-config` named volume at runtime, so user-installed
|
||||||
|
packages survive container recreate AND image rebuild.
|
||||||
|
|
||||||
|
But the *variant build* inherits this prefix at build time. If left as-is,
|
||||||
|
`npm install -g opencode-ai@$VERSION` in `Dockerfile.variant` would
|
||||||
|
install opencode into `/home/developer/.pi/npm-global/...`, which is then
|
||||||
|
**shadowed by the volume mount at runtime** → opencode disappears from
|
||||||
|
PATH on first start.
|
||||||
|
|
||||||
|
Fix: each `npm install -g` in `Dockerfile.variant` overrides the prefix
|
||||||
|
per-RUN:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION}
|
||||||
|
```
|
||||||
|
|
||||||
|
Baked binaries land on `/usr/bin/...` (system prefix), survive the volume
|
||||||
|
mount. Runtime-installed user packages still land on
|
||||||
|
`~/.pi/npm-global/...`. Both visible on PATH.
|
||||||
|
|
||||||
|
## Cache strategy
|
||||||
|
|
||||||
|
Two registry caches are configured:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cache-from: type=registry,ref=joakimp/opencode-devbox:base-buildcache
|
||||||
|
cache-to: type=registry,ref=joakimp/opencode-devbox:base-buildcache,mode=max
|
||||||
|
|
||||||
|
cache-from: type=registry,ref=joakimp/opencode-devbox:base-variant-buildcache
|
||||||
|
cache-to: type=registry,ref=joakimp/opencode-devbox:base-variant-buildcache,mode=max
|
||||||
|
```
|
||||||
|
|
||||||
|
`mode=max` exports cache for *all* layers, not just the final image's
|
||||||
|
layers. Important for multi-arch builds where the cross-arch layer reuse
|
||||||
|
matters more.
|
||||||
|
|
||||||
|
## Wall-clock estimates
|
||||||
|
|
||||||
|
| Scenario | Production pipeline | Split-base pipeline |
|
||||||
|
|---|---|---|
|
||||||
|
| Version-bump-only release (only opencode/pi/omos version changed) | ~165–180 min | **~30–40 min** (base cache hit) |
|
||||||
|
| Base-touching release (apt/Node/Debian/entrypoint change) | ~165–180 min | **~70–90 min** (base rebuilds) |
|
||||||
|
|
||||||
|
The split-base pipeline pays its dues on base-touching releases (which are
|
||||||
|
infrequent — a few times a year for Debian / Node major version bumps).
|
||||||
|
Most releases are version-bumps and ride the cache.
|
||||||
|
|
||||||
|
## Validate workflow
|
||||||
|
|
||||||
|
[`validate.yml`](workflows/validate.yml) is the lightweight gate that runs
|
||||||
|
on every push to `main` and on PRs. It:
|
||||||
|
|
||||||
|
1. Runs `scripts/generate-dockerhub-md.py --check` to enforce
|
||||||
|
`DOCKER_HUB.md` is in sync with `HUB_TEMPLATE`.
|
||||||
|
2. Builds each of the four variants amd64-only (no multi-arch, no push)
|
||||||
|
and runs `scripts/smoke-test.sh`.
|
||||||
|
|
||||||
|
This catches regressions before they reach a tag push. Wall clock ~30 min.
|
||||||
|
|
||||||
|
## Runner expectations
|
||||||
|
|
||||||
|
- **Image:** `catthehacker/ubuntu:act-latest`. Each job runs inside a
|
||||||
|
fresh container of this image. Don't assume any pre-installed
|
||||||
|
toolchains beyond what catthehacker ships.
|
||||||
|
- **Disk pressure:** the runner host has ~40 GB of usable overlay space,
|
||||||
|
often 70%+ used at job start. Every job that does `load: true` (smoke)
|
||||||
|
starts with a `Reclaim runner disk` step that strips
|
||||||
|
catthehacker-resident toolchains (Android SDK, .NET, Swift, GHC, JVM,
|
||||||
|
Boost, Chromium, PowerShell) and prunes stale docker state. Don't
|
||||||
|
remove these steps without testing on a fresh runner.
|
||||||
|
- **Concurrency:** 2 runners. Jobs in the same workflow run can fan out to
|
||||||
|
both; jobs in *different* workflow runs are serialized by gitea's queue.
|
||||||
|
The `concurrency: { group: ${{ workflow }}-${{ ref }}, cancel-in-progress: false }`
|
||||||
|
setting keeps tag pushes from racing each other but allows
|
||||||
|
per-PR/per-branch parallelism.
|
||||||
|
- **Workflow visibility in UI:** gitea Actions only surfaces workflows
|
||||||
|
from the **default branch** in the web UI's workflow list, even for
|
||||||
|
`workflow_dispatch` triggers. Workflows on feature branches are
|
||||||
|
invisible until merged to `main`.
|
||||||
|
- **Disk reclaim quirk:** `actions/{upload,download}-artifact@v4+` does
|
||||||
|
not work on Gitea (depends on a GitHub-only Artifact API). Stick to
|
||||||
|
`@v3` if matrix-fanout-with-artifacts is ever needed. We avoided this
|
||||||
|
by using `docker/build-push-action@v7` with comma-separated
|
||||||
|
`platforms: linux/amd64,linux/arm64` — natively does multi-arch push
|
||||||
|
in a single job, no artifact dance.
|
||||||
|
|
||||||
|
## Migration plan: split-base → production
|
||||||
|
|
||||||
|
1. **Validate the split-base dispatch.** Trigger
|
||||||
|
`docker-publish-split.yml` manually with `release_tag=v0.0.0-split-test`
|
||||||
|
and `promote_latest=false`. Confirm all jobs go green, image sizes
|
||||||
|
match the production baseline within ~10%, and no unexpected layer
|
||||||
|
rebuilds appear in `build-variant-*` logs after the FROM line.
|
||||||
|
2. **Run a second dispatch** to confirm cache-hit behavior:
|
||||||
|
`base-decide` should set `need_build=false`, `build-base` should be
|
||||||
|
skipped entirely, total wall clock should drop to ~25–40 min.
|
||||||
|
3. **Cut over** — *done as of v1.14.50.* `docker-publish-split.yml` now
|
||||||
|
triggers on `push: tags: v*`. `docker-publish.yml` and original
|
||||||
|
`Dockerfile` deleted.
|
||||||
|
4. **Tag a release.** First production release on the new pipeline.
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- [`AGENTS.md`](../AGENTS.md) — domain facts, release-day checklist,
|
||||||
|
documentation coupling rules. Read first when modifying CI behavior.
|
||||||
|
- [`CHANGELOG.md`](../CHANGELOG.md) — build pipeline rewrite landed in v1.14.50.
|
||||||
|
- `Dockerfile.base`, `Dockerfile.variant` — the split-base Dockerfiles.
|
||||||
|
Comments at the top of each explain their role.
|
||||||
|
- [`scripts/smoke-test.sh`](../scripts/smoke-test.sh) — invoked by all
|
||||||
|
three workflows; this is the single source of truth for "what does a
|
||||||
|
built image have to satisfy".
|
||||||
|
- [`scripts/generate-dockerhub-md.py`](../scripts/generate-dockerhub-md.py)
|
||||||
|
— generates `DOCKER_HUB.md` from `HUB_TEMPLATE`. `--check` enforces
|
||||||
|
sync in `validate.yml`.
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
name: Publish Docker Image (split-base)
|
name: Publish Docker Image
|
||||||
|
|
||||||
# Two-phase split-base build pipeline. Lives ALONGSIDE the original
|
# Two-phase split-base build pipeline. Replaces the original
|
||||||
# docker-publish.yml during the migration window. Triggers only on
|
# docker-publish.yml single-Dockerfile pipeline.
|
||||||
# workflow_dispatch (manual) so it doesn't conflict with the production
|
|
||||||
# tag-trigger pipeline.
|
|
||||||
#
|
|
||||||
# Once we've validated 1-2 successful runs and verified output
|
|
||||||
# byte-for-byte against the original, this workflow takes over `on:
|
|
||||||
# push: tags: v*` and the original is retired.
|
|
||||||
#
|
#
|
||||||
# Pipeline shape:
|
# Pipeline shape:
|
||||||
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
|
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
|
||||||
@@ -22,14 +16,17 @@ name: Publish Docker Image (split-base)
|
|||||||
# 6. update-description patch Docker Hub description (unchanged).
|
# 6. update-description patch Docker Hub description (unchanged).
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release_tag:
|
release_tag:
|
||||||
description: 'Release tag to publish (e.g. v1.14.42-split). Pushed to Docker Hub as both the literal tag and `latest*`-aliases.'
|
description: 'Release tag to publish (e.g. v1.14.50). Used only for workflow_dispatch runs — tag-triggered runs derive the tag from github.ref.'
|
||||||
required: true
|
required: false
|
||||||
default: 'v0.0.0-split-test'
|
default: ''
|
||||||
promote_latest:
|
promote_latest:
|
||||||
description: 'Update latest/latest-omos/latest-with-pi/latest-omos-with-pi aliases (set false for test runs)'
|
description: 'Update latest/* aliases (default true for tag-push, set false for manual test runs)'
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
@@ -40,6 +37,8 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
BUILDKIT_PROGRESS: plain
|
BUILDKIT_PROGRESS: plain
|
||||||
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
|
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
|
||||||
|
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }}
|
||||||
|
PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }}
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────
|
||||||
# Reusable disk-reclaim snippet — strips catthehacker toolchains and
|
# Reusable disk-reclaim snippet — strips catthehacker toolchains and
|
||||||
@@ -354,12 +353,14 @@ jobs:
|
|||||||
- name: Compute version-specific tags
|
- name: Compute version-specific tags
|
||||||
id: tags
|
id: tags
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.release_tag }}"
|
VERSION="${{ env.RELEASE_TAG }}"
|
||||||
TAGS="${IMAGE}:${VERSION}"
|
{ echo "tags<<EOF"
|
||||||
if [ "${{ inputs.promote_latest }}" = "true" ]; then
|
echo "${IMAGE}:${VERSION}"
|
||||||
TAGS="${TAGS}\n${IMAGE}:latest"
|
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||||
fi
|
echo "${IMAGE}:latest"
|
||||||
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/build-push-action@v7
|
- uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -400,12 +401,14 @@ jobs:
|
|||||||
- name: Compute version-specific tags
|
- name: Compute version-specific tags
|
||||||
id: tags
|
id: tags
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.release_tag }}"
|
VERSION="${{ env.RELEASE_TAG }}"
|
||||||
TAGS="${IMAGE}:${VERSION}-omos"
|
{ echo "tags<<EOF"
|
||||||
if [ "${{ inputs.promote_latest }}" = "true" ]; then
|
echo "${IMAGE}:${VERSION}-omos"
|
||||||
TAGS="${TAGS}\n${IMAGE}:latest-omos"
|
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||||
fi
|
echo "${IMAGE}:latest-omos"
|
||||||
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/build-push-action@v7
|
- uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -446,12 +449,14 @@ jobs:
|
|||||||
- name: Compute version-specific tags
|
- name: Compute version-specific tags
|
||||||
id: tags
|
id: tags
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.release_tag }}"
|
VERSION="${{ env.RELEASE_TAG }}"
|
||||||
TAGS="${IMAGE}:${VERSION}-with-pi"
|
{ echo "tags<<EOF"
|
||||||
if [ "${{ inputs.promote_latest }}" = "true" ]; then
|
echo "${IMAGE}:${VERSION}-with-pi"
|
||||||
TAGS="${TAGS}\n${IMAGE}:latest-with-pi"
|
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||||
fi
|
echo "${IMAGE}:latest-with-pi"
|
||||||
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/build-push-action@v7
|
- uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -492,12 +497,14 @@ jobs:
|
|||||||
- name: Compute version-specific tags
|
- name: Compute version-specific tags
|
||||||
id: tags
|
id: tags
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ inputs.release_tag }}"
|
VERSION="${{ env.RELEASE_TAG }}"
|
||||||
TAGS="${IMAGE}:${VERSION}-omos-with-pi"
|
{ echo "tags<<EOF"
|
||||||
if [ "${{ inputs.promote_latest }}" = "true" ]; then
|
echo "${IMAGE}:${VERSION}-omos-with-pi"
|
||||||
TAGS="${TAGS}\n${IMAGE}:latest-omos-with-pi"
|
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||||
fi
|
echo "${IMAGE}:latest-omos-with-pi"
|
||||||
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
|
fi
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
- uses: docker/build-push-action@v7
|
- uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -519,12 +526,40 @@ jobs:
|
|||||||
- build-variant-omos
|
- build-variant-omos
|
||||||
- build-variant-with-pi
|
- build-variant-with-pi
|
||||||
- build-variant-omos-with-pi
|
- build-variant-omos-with-pi
|
||||||
if: inputs.promote_latest == 'true'
|
# Skip on cache-hit base builds: when need_build=false, base-latest
|
||||||
|
# already points at the same digest as base-<hash>, so the retag is
|
||||||
|
# a tautology and any transient failure of it is purely cosmetic.
|
||||||
|
# Manual workflow_dispatch with promote_latest=true overrides this
|
||||||
|
# gate as an escape hatch (e.g., if base-latest got hand-deleted).
|
||||||
|
#
|
||||||
|
# `always()` wrapper + explicit base-variant success check protects
|
||||||
|
# against the gitea-Actions default of "skipped need => skip dependent":
|
||||||
|
# a partial-publish run (e.g., omos-with-pi smoke fails) shouldn't
|
||||||
|
# prevent the base-latest alias from advancing on a real base rebuild.
|
||||||
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.build-variant-base.result == 'success' &&
|
||||||
|
(inputs.promote_latest == 'true' ||
|
||||||
|
(github.ref_type == 'tag' && needs.base-decide.outputs.need_build == 'true'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: imjasonh/setup-crane@v0.4
|
# Direct pinned install instead of imjasonh/setup-crane@v0.4. The
|
||||||
|
# action's bootstrap script calls api.github.com/.../releases/latest
|
||||||
|
# to discover the crane version, which periodically rate-limits and
|
||||||
|
# produces tag=null → download from .../download/null/... → 404 →
|
||||||
|
# 'gzip: unexpected end of file' → exit 2. Pinning removes the
|
||||||
|
# runtime dependency on GitHub API entirely. Bump CRANE_VERSION
|
||||||
|
# deliberately when you want updates.
|
||||||
|
- name: Install crane (pinned)
|
||||||
|
env:
|
||||||
|
CRANE_VERSION: v0.21.6
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin crane
|
||||||
|
crane version
|
||||||
- name: Login (crane)
|
- name: Login (crane)
|
||||||
run: |
|
run: |
|
||||||
crane auth login docker.io \
|
crane auth login docker.io \
|
||||||
@@ -543,7 +578,17 @@ jobs:
|
|||||||
- build-variant-omos
|
- build-variant-omos
|
||||||
- build-variant-with-pi
|
- build-variant-with-pi
|
||||||
- build-variant-omos-with-pi
|
- build-variant-omos-with-pi
|
||||||
if: inputs.promote_latest == 'true'
|
# Run when at least the base variant published — don't let a single
|
||||||
|
# variant failure (e.g., omos-with-pi smoke threshold) prevent Hub
|
||||||
|
# description refresh for the other variants that did publish.
|
||||||
|
# Without this `always()` wrapper, gitea Actions' default behavior
|
||||||
|
# of "skipped need => skip dependent" cascades from any failed/
|
||||||
|
# skipped build-variant-* into update-description, and the Hub
|
||||||
|
# description goes stale on partial-publish releases.
|
||||||
|
if: |
|
||||||
|
always() &&
|
||||||
|
needs.build-variant-base.result == 'success' &&
|
||||||
|
(github.ref_type == 'tag' || inputs.promote_latest == 'true')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|||||||
@@ -1,581 +0,0 @@
|
|||||||
name: Publish Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '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:
|
|
||||||
# ── Smoke test (amd64 only, gates the push jobs) ────────────────────
|
|
||||||
smoke-base:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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
|
|
||||||
|
|
||||||
# See docker-publish.yml preamble. `load: true` peak disk = tarball
|
|
||||||
# + 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
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
with:
|
|
||||||
driver-opts: network=host
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
tags: opencode-devbox:smoke-base
|
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
|
||||||
|
|
||||||
smoke-omos:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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 system df || true
|
|
||||||
docker system prune -af --volumes || true
|
|
||||||
docker builder prune -af || true
|
|
||||||
df -h / || true
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
with:
|
|
||||||
driver-opts: network=host
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_OMOS=true
|
|
||||||
tags: opencode-devbox:smoke-omos
|
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
|
||||||
|
|
||||||
smoke-with-pi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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 system df || true
|
|
||||||
docker system prune -af --volumes || true
|
|
||||||
docker builder prune -af || true
|
|
||||||
df -h / || true
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
with:
|
|
||||||
driver-opts: network=host
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_PI=true
|
|
||||||
tags: opencode-devbox:smoke-with-pi
|
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
|
|
||||||
|
|
||||||
smoke-omos-with-pi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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 system df || true
|
|
||||||
docker system prune -af --volumes || true
|
|
||||||
docker builder prune -af || true
|
|
||||||
df -h / || true
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
with:
|
|
||||||
driver-opts: network=host
|
|
||||||
|
|
||||||
- name: Build and load amd64 image for smoke test
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_OMOS=true
|
|
||||||
INSTALL_PI=true
|
|
||||||
tags: opencode-devbox:smoke-omos-with-pi
|
|
||||||
|
|
||||||
- name: Smoke test (amd64)
|
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_OMOS=true
|
|
||||||
tags: |
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos
|
|
||||||
|
|
||||||
build-with-pi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: smoke-with-pi
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_PI=true
|
|
||||||
tags: |
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-with-pi
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-with-pi
|
|
||||||
|
|
||||||
build-omos-with-pi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: smoke-omos-with-pi
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INSTALL_OMOS=true
|
|
||||||
INSTALL_PI=true
|
|
||||||
tags: |
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos-with-pi
|
|
||||||
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos-with-pi
|
|
||||||
|
|
||||||
update-description:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [build-base, build-omos, build-with-pi, build-omos-with-pi]
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Update Docker Hub description
|
|
||||||
run: |
|
|
||||||
TOKEN=$(curl -s -X POST https://hub.docker.com/v2/auth/token \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"identifier":"${{ vars.DOCKERHUB_USERNAME }}","secret":"${{ secrets.DOCKERHUB_TOKEN }}"}' \
|
|
||||||
| jq -r .access_token)
|
|
||||||
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
|
|
||||||
echo "::error::Failed to authenticate with Docker Hub API"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
HTTP_CODE=$(jq -n \
|
|
||||||
--rawfile full DOCKER_HUB.md \
|
|
||||||
--arg short "Portable AI dev environment for opencode. Debian-based with git, Node.js, AWS CLI, and SSH support." \
|
|
||||||
'{"full_description": $full, "description": $short}' | \
|
|
||||||
curl -s -o /tmp/hub-response.txt -w "%{http_code}" -X PATCH \
|
|
||||||
"https://hub.docker.com/v2/repositories/${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox/" \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d @-)
|
|
||||||
echo "Docker Hub API returned: $HTTP_CODE"
|
|
||||||
if [ "$HTTP_CODE" != "200" ]; then
|
|
||||||
echo "Response body:"
|
|
||||||
cat /tmp/hub-response.txt
|
|
||||||
echo "::error::Docker Hub description update failed with HTTP $HTTP_CODE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -2,8 +2,24 @@ name: Validate
|
|||||||
|
|
||||||
# Lightweight validation on pushes to main. Builds single-arch (amd64),
|
# Lightweight validation on pushes to main. Builds single-arch (amd64),
|
||||||
# runs the smoke test, and checks image size — without pushing anything
|
# runs the smoke test, and checks image size — without pushing anything
|
||||||
# to Docker Hub. Tag pushes are handled by docker-publish.yml which
|
# to Docker Hub. Tag pushes are handled by docker-publish-split.yml which
|
||||||
# does the full multi-arch build-and-push.
|
# does the full multi-arch split-base build-and-push.
|
||||||
|
#
|
||||||
|
# Trade-off: variant builds here use the published `base-latest` image
|
||||||
|
# from Docker Hub as their parent, NOT a locally-built base. This is
|
||||||
|
# because `docker/build-push-action@v7` runs each invocation in its own
|
||||||
|
# buildx container context, so an image loaded into the host docker
|
||||||
|
# daemon by step N is not visible to step N+1's buildx invocation.
|
||||||
|
# Building base + variant in the same job would require either pushing
|
||||||
|
# the base to a registry or sharing a buildx instance across steps — both
|
||||||
|
# significantly more complex than just using the published base.
|
||||||
|
#
|
||||||
|
# Consequence: PRs/pushes that change Dockerfile.base, rootfs/, or
|
||||||
|
# entrypoint*.sh are NOT exercised by this workflow. The release path
|
||||||
|
# (docker-publish-split.yml on tag push) does build the new base, so
|
||||||
|
# release tags are the gate that fully validates base-image changes.
|
||||||
|
# The base-change-warning job below surfaces a runtime warning when this
|
||||||
|
# blind-spot applies.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -34,6 +50,33 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python3 scripts/generate-dockerhub-md.py --check
|
python3 scripts/generate-dockerhub-md.py --check
|
||||||
|
|
||||||
|
base-change-warning:
|
||||||
|
# Surfaces a warning when this commit changes base-image inputs
|
||||||
|
# (Dockerfile.base, rootfs/, entrypoint*.sh). validate.yml uses
|
||||||
|
# Hub's base-latest as the parent for variant builds, so changes to
|
||||||
|
# those files are NOT exercised here — only release tags rebuild the
|
||||||
|
# base via docker-publish-split.yml.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Detect base-input changes
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if ! git diff --name-only HEAD~1 HEAD 2>/dev/null \
|
||||||
|
| grep -qE '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'; then
|
||||||
|
echo "No base-image inputs changed in this commit — validate.yml fully exercises the published base-latest."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "::warning::This commit changes base-image inputs (Dockerfile.base, rootfs/, or entrypoint*.sh). validate.yml uses Hub's base-latest as the parent for variant builds, so the new base is NOT exercised by this workflow. Cut a release tag, or run a workflow_dispatch of docker-publish-split.yml against a test tag (e.g. v0.0.0-base-test, promote_latest=false) for end-to-end validation of the new base."
|
||||||
|
echo "Changed base-input files:"
|
||||||
|
git diff --name-only HEAD~1 HEAD | grep -E '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'
|
||||||
|
|
||||||
validate-base:
|
validate-base:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
@@ -83,9 +126,12 @@ jobs:
|
|||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile.variant
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
|
build-args: |
|
||||||
|
BASE_IMAGE=joakimp/opencode-devbox:base-latest
|
||||||
tags: opencode-devbox:ci-base
|
tags: opencode-devbox:ci-base
|
||||||
|
|
||||||
- name: Smoke test
|
- name: Smoke test
|
||||||
@@ -137,10 +183,12 @@ jobs:
|
|||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile.variant
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
build-args: |
|
build-args: |
|
||||||
|
BASE_IMAGE=joakimp/opencode-devbox:base-latest
|
||||||
INSTALL_OMOS=true
|
INSTALL_OMOS=true
|
||||||
tags: opencode-devbox:ci-omos
|
tags: opencode-devbox:ci-omos
|
||||||
|
|
||||||
@@ -193,10 +241,12 @@ jobs:
|
|||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile.variant
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
build-args: |
|
build-args: |
|
||||||
|
BASE_IMAGE=joakimp/opencode-devbox:base-latest
|
||||||
INSTALL_PI=true
|
INSTALL_PI=true
|
||||||
tags: opencode-devbox:ci-with-pi
|
tags: opencode-devbox:ci-with-pi
|
||||||
|
|
||||||
@@ -249,10 +299,12 @@ jobs:
|
|||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile.variant
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
build-args: |
|
build-args: |
|
||||||
|
BASE_IMAGE=joakimp/opencode-devbox:base-latest
|
||||||
INSTALL_OMOS=true
|
INSTALL_OMOS=true
|
||||||
INSTALL_PI=true
|
INSTALL_PI=true
|
||||||
tags: opencode-devbox:ci-omos-with-pi
|
tags: opencode-devbox:ci-omos-with-pi
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Two image variants (base and omos) are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfile, entrypoint scripts, docker-compose, documentation).
|
Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Image variants are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfiles, entrypoint scripts, docker-compose, documentation).
|
||||||
|
|
||||||
## File roles
|
## File roles
|
||||||
|
|
||||||
- `Dockerfile` — production single-Dockerfile build. Variants are gated by build args: `INSTALL_OMOS` (Bun + multi-agent layer), `INSTALL_OPENCODE` (default true), `INSTALL_PI` (default false), `INSTALL_MEMPALACE` (default true). All GitHub-sourced binaries are pinned with version ARGs.
|
- `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes.
|
||||||
- `Dockerfile.base` and `Dockerfile.variant` — **WIP, branch `feat/split-build` only.** Two-Dockerfile split-base build: base contains all variant-independent layers; variant `FROM`s the base and adds only opencode/omos/pi installs. Used by `docker-publish-split.yml` (workflow_dispatch only) for parallel testing alongside the production pipeline. See CHANGELOG `Unreleased` for the migration plan and trade-offs.
|
- `Dockerfile.variant` — `FROM`s the base and adds only opencode/omos/pi installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, `INSTALL_PI`, and `INSTALL_MEMPALACE`. 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. Volume ownership loop covers `~/.pi/` when `INSTALL_PI=true`.
|
- `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. Volume ownership loop covers `~/.pi/` when `INSTALL_PI=true`.
|
||||||
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
|
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
|
||||||
- `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).
|
- `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).
|
||||||
@@ -15,22 +15,22 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--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 a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
||||||
- `DOCKER_HUB.md` — **auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. 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 `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. 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.
|
||||||
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
|
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
|
||||||
|
- `.gitea/README.md` — **read this first** if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan.
|
||||||
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
|
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
|
||||||
- `.gitea/workflows/docker-publish.yml` — production CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description.
|
- `.gitea/workflows/docker-publish-split.yml` — production CI pipeline on tag push (`v*`). Two-phase split-base: computes base hash, conditionally builds base, runs 4 parallel smoke tests, then 4 parallel multi-arch variant builds, promotes `base-latest` alias, updates Docker Hub description.
|
||||||
- `.gitea/workflows/docker-publish-split.yml` — **WIP, branch `feat/split-build` only.** Two-phase split-base pipeline. Triggers on `workflow_dispatch` only so it runs alongside the production pipeline without conflict. Pushes to user-supplied `release_tag` input (e.g. `v0.0.0-split-test`); `latest*` aliases only updated when `promote_latest: true`. Compute base hash, conditionally build base, then 4 variant deltas in parallel.
|
|
||||||
|
|
||||||
## Versioning scheme
|
## Versioning scheme
|
||||||
|
|
||||||
Tags follow `v{opencode_version}[letter]` — e.g. `v1.14.20` for the first build on a new opencode release, and `v1.14.20b`, `v1.14.20c`, … for subsequent rebuilds on the same opencode version.
|
Tags follow `v{opencode_version}[letter]` — e.g. `v1.14.20` for the first build on a new opencode release, and `v1.14.20b`, `v1.14.20c`, … for subsequent rebuilds on the same opencode version.
|
||||||
|
|
||||||
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile`).
|
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile.variant`).
|
||||||
- **No letter suffix** on the first build of a new opencode version — the bare `v{opencode_version}` tag is the canonical release.
|
- **No letter suffix** on the first build of a new opencode version — the bare `v{opencode_version}` tag is the canonical release.
|
||||||
- **Letter suffix is the build ordinal**, starting at `b` for the second build. The letter `a` is **never used** — think of the suffix as counting rebuilds: `b = 2nd, c = 3rd, d = 4th, …`. For opencode version `1.14.20`: first build `v1.14.20`, second `v1.14.20b`, third `v1.14.20c`, and so on.
|
- **Letter suffix is the build ordinal**, starting at `b` for the second build. The letter `a` is **never used** — think of the suffix as counting rebuilds: `b = 2nd, c = 3rd, d = 4th, …`. For opencode version `1.14.20`: first build `v1.14.20`, second `v1.14.20b`, third `v1.14.20c`, and so on.
|
||||||
- A letter suffix is only used for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.
|
- A letter suffix is only used for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.
|
||||||
|
|
||||||
CI produces eight Docker Hub tags per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`, `vX.Y.Z[n]-with-pi`, `latest-with-pi`, `vX.Y.Z[n]-omos-with-pi`, `latest-omos-with-pi` — one tag pair (versioned + floating alias) per build variant.
|
CI produces eight Docker Hub tags per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`, `vX.Y.Z[n]-with-pi`, `latest-with-pi`, `vX.Y.Z[n]-omos-with-pi`, `latest-omos-with-pi` — one tag pair (versioned + floating alias) per build variant.
|
||||||
|
|
||||||
When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile` and update the comment in `.env.example` if it names a specific model/version for context.
|
When bumping the opencode version, bump `OPENCODE_VERSION` in `Dockerfile.variant` and update the comment in `.env.example` if it names a specific model/version for context.
|
||||||
|
|
||||||
## Critical conventions
|
## Critical conventions
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- **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.
|
- **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.
|
- **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. **Same pattern for pi:** `devbox-pi-config` is mounted at `~/.pi/` and persists user toggles (`/ext`-disabled extensions), `~/.pi/agent/settings.json` edits, and — because `NPM_CONFIG_PREFIX` is set to `~/.pi/npm-global` — anything installed via `pi install npm:...` or `npm install -g` as the developer user, across container recreate AND image rebuild.
|
- **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. **Same pattern for pi:** `devbox-pi-config` is mounted at `~/.pi/` and persists user toggles (`/ext`-disabled extensions), `~/.pi/agent/settings.json` edits, and — because `NPM_CONFIG_PREFIX` is set to `~/.pi/npm-global` — anything installed via `pi install npm:...` or `npm install -g` as the developer user, across container recreate AND image rebuild.
|
||||||
- **pi install contract** — `INSTALL_PI=true` (default false) opt-in build arg. The baked `pi` binary is npm-installed globally to `/usr` at build time (system prefix). At runtime, `NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global` is set in the image ENV with that prefix's `bin/` prepended to `PATH` — so any `pi install npm:...` or `npm install -g` invoked by the developer user lands on the named volume and survives everything except `docker compose down -v`. The new ENVs are declared *after* all build-time `npm install -g` calls in the Dockerfile so they don't redirect the baked installs into a path that the volume mount would later shadow. If the user runs `npm install -g @mariozechner/pi-coding-agent` themselves, the user-installed copy on the volume wins via `PATH` order; otherwise image rebuild is the upgrade path for the baked pi (same contract as `OPENCODE_VERSION`). The pi-toolkit and pi-extensions repos are git-cloned into `/opt/` at build time, then their `install.sh` runs from `entrypoint-user.sh` on each container start to symlink into `~/.pi/agent/` (which lives on the named volume). The mempalace pi-bridge is symlinked manually from `/opt/mempalace-toolkit/extensions/pi/mempalace.ts` — we do NOT call mempalace-toolkit's full `install.sh` because its `install_skill` step would race with skillset auto-deploy `--prune-stale`.
|
- **pi install contract** — `INSTALL_PI=true` (default false) opt-in build arg. The baked `pi` binary is npm-installed globally to `/usr` at build time (system prefix). At runtime, `NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global` is set in the image ENV with that prefix's `bin/` prepended to `PATH` — so any `pi install npm:...` or `npm install -g` invoked by the developer user lands on the named volume and survives everything except `docker compose down -v`. The new ENVs are declared *after* all build-time `npm install -g` calls in the Dockerfile so they don't redirect the baked installs into a path that the volume mount would later shadow. If the user runs `npm install -g @earendil-works/pi-coding-agent` themselves, the user-installed copy on the volume wins via `PATH` order; otherwise image rebuild is the upgrade path for the baked pi (same contract as `OPENCODE_VERSION`). The pi-toolkit and pi-extensions repos are git-cloned into `/opt/` at build time, then their `install.sh` runs from `entrypoint-user.sh` on each container start to symlink into `~/.pi/agent/` (which lives on the named volume). The mempalace pi-bridge is symlinked manually from `/opt/mempalace-toolkit/extensions/pi/mempalace.ts` — we do NOT call mempalace-toolkit's full `install.sh` because its `install_skill` step would race with skillset auto-deploy `--prune-stale`.
|
||||||
- **Pi deploy ordering matters in entrypoint-user.sh** — `pi-toolkit` runs first (creates `keybindings.json` symlink and writes pi-env.zsh), then `pi-extensions`, then `settings.json` template bootstrap, then mempalace bridge symlink. mempalace-toolkit's `check_pi_toolkit` probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.
|
- **Pi deploy ordering matters in entrypoint-user.sh** — `pi-toolkit` runs first (creates `keybindings.json` symlink and writes pi-env.zsh), then `pi-extensions`, then `settings.json` template bootstrap, then mempalace bridge symlink. mempalace-toolkit's `check_pi_toolkit` probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.
|
||||||
- **Default CMD is `bash -l`** — not a harness. `docker compose run --rm devbox` drops the user into a login shell to choose: `aws sso login`, then `opencode` or `pi` (or any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode` / `docker compose run --rm devbox pi`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
|
- **Default CMD is `bash -l`** — not a harness. `docker compose run --rm devbox` drops the user into a login shell to choose: `aws sso login`, then `opencode` or `pi` (or any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode` / `docker compose run --rm devbox pi`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
|
||||||
- **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.
|
||||||
@@ -65,7 +65,7 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- **`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).
|
- **`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.
|
- **`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.
|
- **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.
|
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish-split.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
|
||||||
|
|
||||||
|
|||||||
+105
-14
@@ -8,32 +8,123 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
Docs-only updates that don't trigger a new image build (Docker Hub description was patched live via the API without re-tagging):
|
## v1.15.4b — 2026-05-18
|
||||||
|
|
||||||
- **Docs:** DOCKER_HUB.md `Image Variants` table now lists all four published variants (`latest`, `latest-omos`, `latest-with-pi`, `latest-omos-with-pi`) instead of only the first two. Generator (`scripts/generate-dockerhub-md.py`) HEADER updated to match.
|
Recovery release for v1.15.4 — the `omos-with-pi` variant landed at >3500 MB and tripped the smoke threshold, so `smoke-omos-with-pi` and `build-variant-omos-with-pi` were skipped. The other three variants (base, omos, with-pi) published cleanly. Plus a latent workflow bug fix exposed by the partial publish.
|
||||||
- **Docs:** DOCKER_HUB.md gains a tailored `pi (alternative/complementary harness)` section covering run, mempalace integration, and persistence — the full README pi section is too large to include verbatim under the 25 kB Hub limit, so a `replace` rule in the generator emits a Hub-tailored excerpt that links out to the gitea README anchor for full build args / extension list / toolkit detail. DOCKER_HUB.md size 24 862 bytes (138 byte headroom).
|
|
||||||
- **Docs:** README pi section gains a `### Setup` paragraph mentioning the prebuilt `latest-with-pi` and `latest-omos-with-pi` Docker Hub tags, mirroring the OMOS section's `latest-omos` mention.
|
|
||||||
- **Docs:** AGENTS.md tag-scheme paragraph corrected from "four Docker Hub tags per release" to eight (the v1.14.41b CI matrix expansion). Reclaim-disk job list updated from the four pre-pi jobs to all eight current `load: true` jobs.
|
|
||||||
|
|
||||||
Image changes (will ship in the next tagged release):
|
- **Smoke threshold bump:** `omos-with-pi` 3500 → 3700 MB. Compounded growth: opencode 1.15.0 → 1.15.4 (4 patch versions) plus pi 0.74.0 → 0.75.3 (minor + 3 patches) both added a few MB each, and they sum in the omos-with-pi variant. Same pattern as previous threshold bumps (v1.14.31c, v1.15.0b); restores ~150 MB headroom.
|
||||||
|
- **Workflow fix — `update-description` no longer skips on partial publish.** Pre-existing latent bug: `update-description.needs` includes all four `build-variant-*` jobs, and gitea Actions' default behavior is "skipped need ⇒ skip dependent". When `build-variant-omos-with-pi` got skipped (because its smoke failed), `update-description` cascaded into a skip even though the job's `if:` condition (`tag pushed`) was true. Result: Hub description wasn't refreshed on v1.15.4 despite three variants publishing. Fix: wrap the `if:` in `always() && needs.build-variant-base.result == 'success' && ...` so the job runs as long as the base variant published, regardless of what other variants did.
|
||||||
|
- **Same fix applied to `promote-base-latest`** — had the identical latent bug. Currently masked by the cache-hit skip, but would have surfaced on a real-base-rebuild release with a single failed variant.
|
||||||
|
- No image-side changes from v1.15.4. Cache hit on the same base hash (`base-35ee5fe7861a`).
|
||||||
|
|
||||||
|
## v1.15.4 — 2026-05-18
|
||||||
|
|
||||||
|
opencode 1.15.3 → 1.15.4 bump (one upstream patch release), bundled with the CI hardening that landed on main between v1.15.3 and now.
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.15.3 → 1.15.4 (`OPENCODE_VERSION` in `Dockerfile.variant`).
|
||||||
|
- **CI: pinned crane install in `promote-base-latest`.** Replaced `imjasonh/setup-crane@v0.4` with a direct `curl + tar` install pinned to crane v0.21.6. The action's bootstrap script calls `api.github.com/.../releases/latest` to discover what crane version to install. That call periodically rate-limits and produces `tag=null` → the action downloads `releases/download/null/...` → 404 → `gzip: unexpected end of file` → exit 2. We hit this on v1.15.3 (cosmetic failure since base-latest was already correct from cache hit). Pinned install removes the runtime GitHub API dependency entirely. Bump `CRANE_VERSION` deliberately when wanting updates, same pattern as the other GitHub-sourced binaries in the Dockerfile layer.
|
||||||
|
- **CI: skip `promote-base-latest` on cache-hit base builds.** When the base layer hash hasn't changed (cache-hit on the existing `base-<hash>` from a previous run), `base-latest` already points at the correct digest, so the retag is a tautology. Job now skipped entirely when `needs.base-decide.outputs.need_build == 'false'`. Manual `workflow_dispatch` with `promote_latest: true` overrides the gate as an escape hatch for hand-recovery scenarios.
|
||||||
|
- No image-side changes from the v1.15.3 baseline beyond the opencode npm version. Smoke thresholds unchanged.
|
||||||
|
|
||||||
|
## v1.15.3 — 2026-05-16
|
||||||
|
|
||||||
|
opencode 1.15.0 → 1.15.3 bump (three upstream patch releases).
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.15.0 → 1.15.3 (`OPENCODE_VERSION` in `Dockerfile.variant`).
|
||||||
|
- No container-side changes. Smoke thresholds from v1.15.0b unchanged.
|
||||||
|
|
||||||
|
## v1.15.0b — 2026-05-15
|
||||||
|
|
||||||
|
Rebuild of v1.15.0 with one fix — v1.15.0's `omos` variant landed at 3206 MB, 6 MB over the 3200 MB smoke threshold, so `smoke-omos` failed and `build-variant-omos` was skipped. opencode 1.15.0 grew slightly vs 1.14.50, leaving zero headroom on the existing threshold.
|
||||||
|
|
||||||
|
- **Smoke threshold bump:** `omos` 3200 → 3300 MB, `omos-with-pi` 3400 → 3500 MB. Restores ~100 MB headroom for routine apt-get upgrade drift between releases. Documented inline in `scripts/smoke-test.sh`. No image-side changes — cache hits across the board, just a re-publish on the bumped threshold.
|
||||||
|
|
||||||
|
## v1.15.0 — 2026-05-15
|
||||||
|
|
||||||
|
opencode 1.14.50 → 1.15.0 bump (upstream minor release).
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.14.50 → 1.15.0 (`OPENCODE_VERSION` in `Dockerfile.variant`).
|
||||||
|
- **Resilience:** `git clone` for pi-toolkit and pi-extensions in `Dockerfile.variant` is now wrapped in a 5-attempt retry loop with linear backoff (5s, 10s, 15s, 20s, 25s = up to ~75s total). gitea.jordbo.se occasionally returns transient HTTP 500s on the first request after idle, which previously broke the with-pi and omos-with-pi variant builds. Same pattern landed in pi-devbox repo concurrently.
|
||||||
|
- **Docs:** `DOCKER_HUB.md` mentions `joakimp/pi-devbox` as a sibling image — the pi-only build that uses this image's base layer as its parent. Generator template (`scripts/generate-dockerhub-md.py`) updated and regenerated. Hub size: 5905 bytes (well under the 25 kB limit).
|
||||||
|
- **Recovery from v1.14.50c partial publish:** the `latest-omos`, `v1.14.50c-omos` Hub gap is closed by this release — `latest-omos` will move forward to v1.15.0 once all four variants publish cleanly. Users on the floating tag were unaffected (still pointing at v1.14.41b until now).
|
||||||
|
|
||||||
|
## v1.14.50c — 2026-05-14
|
||||||
|
|
||||||
|
Recovery release for v1.14.50b's missing variants. v1.14.50b shipped only the `base` variant; `omos`, `with-pi`, and `omos-with-pi` were lost to a runner-fleet incident (see postmortem below).
|
||||||
|
|
||||||
|
No container-side changes. This is a tag-only retag to re-run the build on a now-healthy runner fleet. Same `base-35ee5fe7861a` from v1.14.50b is reused via hash-cache hit; only the four variant deltas are rebuilt and published.
|
||||||
|
|
||||||
|
### Postmortem: v1.14.50 / v1.14.50b runner-fleet incident
|
||||||
|
|
||||||
|
Two orthogonal runner-host issues compounded across runs 285–291:
|
||||||
|
|
||||||
|
1. **AVX-less runner shadowing the new fleet.** A pre-migration `act_runner` container on `nyvaken` (Sandy Bridge E3-12xx, has AVX but no AVX2; 4 weeks old, name `act_runner-runner-1`) collided with the orchestrator's freshly deployed `runner-1` VM (Broadwell-EP host, fully AVX2-capable). Gitea scheduled jobs to both. Jobs landing on the nyvaken container `npm install -g opencode-ai@1.14.50` succeeded, then ran `opencode --version` postinstall → the bundled Bun (v1.3.13 baseline) emitted `CPU lacks AVX support`, panicked, and SIGILLed (exit code 132).
|
||||||
|
2. **Containerd shared-state race at `capacity: 2`.** The new VM-based runners initially ran `act_runner` with `capacity: 2`, scheduling two concurrent jobs on a single host. Both jobs would invoke `docker/setup-buildx-action@v4`, which pulls `moby/buildkit:buildx-stable-1`. Containerd's content store raced on identical sha256 ingestion, surfacing as `commit failed: rename .../ingest/.../data .../blobs/sha256/...: no such file or directory` or `failed to extract layer: failed to Lchown ...`.
|
||||||
|
|
||||||
|
A secondary issue surfaced: **Proxmox VM `cpu:` field defaults mask AVX**. The newly-cloned runner VMs had no explicit `cpu:` line in `qm config` and inherited Proxmox's recent default `x86-64-v2-AES`, which excludes AVX even though the Broadwell-EP host silicon has full `avx2`. Fix: `qm set <vmid> --cpu x86-64-v3` (or `host` for full passthrough), then `qm shutdown` + `qm start` (live reboot is not enough). Verified inside guest with `grep -m1 -oE 'avx[2]?' /proc/cpuinfo`.
|
||||||
|
|
||||||
|
Additionally, when `promote-base-latest`'s `needs:` graph requires *all four* `build-variant-*` jobs to succeed, partial publishes leave the `base-latest` Hub alias never advancing. Workaround used during recovery: manually re-tag the new base hash via Docker Hub registry manifest API (`PUT /v2/<repo>/manifests/base-latest` with the body of `GET /v2/<repo>/manifests/base-<sha>`) using a granular Hub PAT. No blob copy needed since blobs are content-addressed.
|
||||||
|
|
||||||
|
### Recovery actions taken (orchestrator + this repo)
|
||||||
|
|
||||||
|
- Orchestrator (cloud-init + ansible repos): set explicit `cpu_type: x86-64-v3` in all runner host yaml files; provision.sh now applies `qm set --cpu` after clone; added runner-3 on proxmox003 for anti-affinity (one runner per Proxmox node); dropped `capacity: 2 → 1` on all runners; bumped `act_runner` 0.3.1 → 0.6.1 across the fleet; documented the CPU-type gotcha as gotcha #9 in cloud-init AGENTS.md and a section in proxmox-guide.md.
|
||||||
|
- User: retired the legacy `act_runner-runner-1` container on nyvaken; cleaned up stale runner registrations in Gitea Site Admin → Actions → Runners.
|
||||||
|
- This repo: no changes needed in Dockerfile.base / Dockerfile.variant; v1.14.50c is a tag-only retag.
|
||||||
|
|
||||||
|
### Fleet state at v1.14.50c
|
||||||
|
|
||||||
|
3 runners (runner-1@proxmox001, runner-2@proxmox002, runner-3@proxmox003), all `act_runner` v0.6.1, all `capacity: 1`, all expose AVX + AVX2 to the guest. No name collisions. Estimated wall clock for v1.14.50c (cache-hit base, 4 variant deltas across 3 runners with capacity:1): ~40–50 min.
|
||||||
|
|
||||||
|
## v1.14.50b — 2026-05-14
|
||||||
|
|
||||||
|
Rebuild of v1.14.50 with two fixes — the v1.14.50 release was incomplete (smokes failed under containerd contention; build-variant jobs skipped; base-latest never promoted to Docker Hub).
|
||||||
|
|
||||||
|
- **Force fresh base rebuild.** Added a `BASE_REBUILD_DATE` comment header to `Dockerfile.base` to invalidate the content hash and trigger a full base rebuild. Picks up ~5 days of Debian trixie security updates and other apt-tracked packages. The comment also documents the pattern for future intentional base-rebuilds without other code changes (recommended cadence: once per release).
|
||||||
|
- **First publish of `base-latest` alias.** `promote-base-latest` runs unconditionally on tag push (`PROMOTE_LATEST=true`), so this release is the first to put `joakimp/opencode-devbox:base-latest` on Docker Hub. Required before pi-devbox (and any other downstream image FROMing the base) can build.
|
||||||
|
|
||||||
|
## v1.14.50 — 2026-05-14
|
||||||
|
|
||||||
|
opencode 1.14.44 → 1.14.50 bump. First release on the split-base build pipeline.
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.14.44 → 1.14.50 (`OPENCODE_VERSION` in `Dockerfile.variant`).
|
||||||
|
- **Infrastructure: split-base pipeline cutover.** `Dockerfile.base` + `Dockerfile.variant` replace the single `Dockerfile`. `docker-publish-split.yml` (now renamed to `docker-publish.yml` in spirit — triggers on `push: tags: v*`) replaces the old `docker-publish.yml`. The original `Dockerfile` and `docker-publish.yml` are deleted. Hash-driven base reuse: version-bump-only releases skip the base build entirely (~40–80 min wall clock with 4 runners vs ~165–180 min previously). Validated across two `workflow_dispatch` test runs (`:v0.0.0-split-test` tags on Docker Hub).
|
||||||
|
- **Fix:** `echo -e` heredoc replaced with POSIX-compatible brace-block for multiline `$GITHUB_OUTPUT` writes in the four `build-variant-*` jobs. `echo -e` does not interpret `\n` in `/bin/sh` (dash), causing `steps.tags.outputs.tags` to be empty and buildx to fail with "tag is needed when pushing to registry".
|
||||||
|
- **Docs:** New `.gitea/README.md` — architectural overview of the split-base pipeline, hash logic, wall-clock estimates, runner expectations, and the migration plan.
|
||||||
|
|
||||||
|
## v1.14.44 — 2026-05-09
|
||||||
|
|
||||||
|
opencode 1.14.42 → 1.14.44 bump (1.14.43 skipped upstream). Also completes the matrix coverage that v1.14.42 missed: `build-omos-with-pi` failed mid-publish on v1.14.42 due to an upstream npm CDN propagation race — `oh-my-opencode-slim@1.0.7` had been published declaring a dependency on `@opencode-ai/sdk@1.14.44`, and our build hit the registry within ~2 minutes of that SDK version landing, before the tarball had propagated across npm's CDN. The build returned 404 on the SDK fetch even though the manifest's `dist-tags.latest` already pointed at 1.14.44. Tarball is now fully fetchable; v1.14.44 builds cleanly across all four variants.
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.14.42 → 1.14.44 (`OPENCODE_VERSION` build-arg default in both `Dockerfile` and `Dockerfile.variant`).
|
||||||
|
|
||||||
|
Known gap: `joakimp/opencode-devbox:v1.14.42-omos-with-pi` and the corresponding `latest-omos-with-pi` alias were NOT published in the v1.14.42 release (`build-omos-with-pi` job failed for the reason above). `latest-omos-with-pi` continued pointing at v1.14.41b until v1.14.44 published. Users on the `latest-omos-with-pi` floating tag were unaffected; users pulling explicit `:v1.14.42-omos-with-pi` would get a 404 from Hub. Closed by v1.14.44.
|
||||||
|
|
||||||
|
## v1.14.42 — 2026-05-09
|
||||||
|
|
||||||
|
**Note:** Of the 4 multi-arch variants, 3 published cleanly (`v1.14.42`, `v1.14.42-omos`, `v1.14.42-with-pi`, plus their `latest*` aliases). `build-omos-with-pi` failed during the publish step due to an upstream npm CDN propagation race (see v1.14.44 entry above for detail). Re-running the failed job would have required another full ~3h matrix rerun in gitea Actions; we chose to bump opencode to 1.14.44 instead and let the next tag close the gap.
|
||||||
|
|
||||||
|
opencode 1.14.41 → 1.14.42 bump. Carries along all container-side changes accumulated since v1.14.41b: pi package rename to `@earendil-works/*`, npm-prefix-on-volume fix, Hub doc rewrite, README/AGENTS docs catchup.
|
||||||
|
|
||||||
|
Image changes:
|
||||||
|
|
||||||
|
- **Bump:** opencode 1.14.41 → 1.14.42 (`OPENCODE_VERSION` build-arg default in both `Dockerfile` and `Dockerfile.variant`).
|
||||||
|
- **Rename:** `npm install -g @mariozechner/pi-coding-agent` -> `npm install -g @earendil-works/pi-coding-agent` in the `INSTALL_PI=true` build path. Pi moved to its new home at earendil-works on 2026-05-07 (https://pi.dev/news/2026/5/7/pi-has-a-new-home); the old `@mariozechner/*` packages are deprecated on npm with the explicit message 'please use @earendil-works/pi-coding-agent instead going forward', and the version stream has moved on (old top-out 0.73.1; new currently 0.74.0). Anyone npm-installing the old name today gets a deprecation warning + a stale binary. Affects both `Dockerfile` (production single-Dockerfile path) and `Dockerfile.variant` (split-base path on main). README, AGENTS, and `HUB_TEMPLATE` URL refs updated from `github.com/mariozechner/pi-coding-agent` (which now 404s) to `github.com/earendil-works/pi`. Brew install references (`brew install pi-coding-agent`) left as-is: formula still works at 0.73.1 and a homebrew tap update is tracked upstream at earendil-works/pi#2755.
|
||||||
- **Fix:** `pi install npm:<pkg>` (and any `npm install -g`) by the `developer` user no longer EACCES against the system npm prefix. `NPM_CONFIG_PREFIX` is now `/home/developer/.pi/npm-global` and the prefix's `bin/` is prepended to `PATH`. The directory lives on the `devbox-pi-config` named volume, so user-installed pi packages (themes, skills, extensions) survive container recreation and image rebuilds. Build-time `npm install -g` calls (opencode, pi, oh-my-opencode-slim) are unaffected because the new ENVs are declared after those steps in the Dockerfile, so the baked binaries still install to `/usr` and are not shadowed by the volume mount.
|
- **Fix:** `pi install npm:<pkg>` (and any `npm install -g`) by the `developer` user no longer EACCES against the system npm prefix. `NPM_CONFIG_PREFIX` is now `/home/developer/.pi/npm-global` and the prefix's `bin/` is prepended to `PATH`. The directory lives on the `devbox-pi-config` named volume, so user-installed pi packages (themes, skills, extensions) survive container recreation and image rebuilds. Build-time `npm install -g` calls (opencode, pi, oh-my-opencode-slim) are unaffected because the new ENVs are declared after those steps in the Dockerfile, so the baked binaries still install to `/usr` and are not shadowed by the volume mount.
|
||||||
|
- **Fix (smoke-test):** `scripts/smoke-test.sh` `oh-my-opencode-slim` check now invokes `npm ls -g` with `NPM_CONFIG_PREFIX=/usr` so it queries the system prefix where the baked install lives. Latent regression from the npm-prefix fix above: default `npm ls -g` started querying the user prefix (`/home/developer/.pi/npm-global`, empty at build time) and missed the baked OMOS install — surfaced when `validate.yml` ran on main after the merge of `feat/split-build`.
|
||||||
|
|
||||||
Build pipeline (branch `feat/split-build`, not yet merged):
|
Docs:
|
||||||
|
|
||||||
- **New: split-base build pipeline.** `Dockerfile.base` (variant-independent layers — apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints) builds once and is published as `joakimp/opencode-devbox:base-<sha>`. `Dockerfile.variant` `FROM`s that base and adds only opencode/omos/pi installs (or skips them per build-args). Companion workflow `.gitea/workflows/docker-publish-split.yml` runs as a `workflow_dispatch`-only pipeline alongside the existing `docker-publish.yml` so they don't conflict. Hash-driven base reuse: a content hash of `Dockerfile.base + rootfs/ + entrypoint*.sh` becomes the base tag; if the tag already exists on Docker Hub, the base build is skipped entirely. Estimated wall clock: version-bump-only release ~30–40 min (vs ~165–180 min today); base-touching release ~60–70 min. Trade-off: two Dockerfiles to maintain, and `npm install -g` in the variant must override `NPM_CONFIG_PREFIX=/usr` per-RUN to keep baked binaries off the volume-shadowed path. Once 1–2 successful workflow_dispatch runs validate the output against the existing pipeline, the new workflow takes over `on: push: tags: v*` and the original is retired.
|
- **Docs:** `DOCKER_HUB.md` `Image Variants` table now lists all four published variants (`latest`, `latest-omos`, `latest-with-pi`, `latest-omos-with-pi`) instead of only the first two. Generator (`scripts/generate-dockerhub-md.py`) HEADER updated to match.
|
||||||
|
- **Docs:** `DOCKER_HUB.md` is now generated from a hand-maintained `HUB_TEMPLATE` constant in `scripts/generate-dockerhub-md.py` instead of a section-by-section transformation of `README.md`. Drops from 24 997 bytes (3 byte headroom) to ~5.5 kB (~78% headroom). The old derive-from-README mechanism (`SECTION_RULES`, `TRIM_SUBSECTIONS`, `REPLACEMENTS`, `split_sections`, `trim_subsections`) is gone — README and Hub doc are now independent surfaces, and most README edits no longer require regenerating `DOCKER_HUB.md`. Trade-off: image-variants table and quick-start flow are now coupled to `HUB_TEMPLATE` and need a manual edit when they change.
|
||||||
Docs-only (the DOCKER_HUB.md change can be patched live to Hub without a CI rebuild; AGENTS.md change is repo-internal):
|
- **Docs:** README pi section gains a `### Setup` paragraph mentioning the prebuilt `latest-with-pi` and `latest-omos-with-pi` Docker Hub tags, mirroring the OMOS section's `latest-omos` mention. "What gets installed" updated to reflect the actual shipped state: 7 pi-extensions (was stale at 6 — mcp-loader was added in pi-extensions but not propagated here), each with a one-line description; mcp-loader gets a paragraph covering its dual-transport (local stdio + remote streamable-HTTP per MCP spec 2025-03-26) capability and the `/mcp` slash command. Clarified that the mempalace bridge is a separate MCP entry point that coexists with mcp-loader rather than being replaced by it.
|
||||||
|
- **Docs:** AGENTS.md tag-scheme paragraph corrected from "four Docker Hub tags per release" to eight (the v1.14.41b CI matrix expansion). "Documentation coupling on release" rule updated — README edits no longer require regenerating `DOCKER_HUB.md`. Release-day checklist tightened.
|
||||||
- **Hub doc rewrite:** `DOCKER_HUB.md` is now generated from a hand-maintained `HUB_TEMPLATE` constant in `scripts/generate-dockerhub-md.py` instead of a section-by-section transformation of `README.md`. Drops from 24 997 bytes (3 byte headroom) to 5 551 bytes (~78% headroom). The old derive-from-README mechanism (`SECTION_RULES`, `TRIM_SUBSECTIONS`, `REPLACEMENTS`, `split_sections`, `trim_subsections`) is gone — README and Hub doc are now independent surfaces. Hub copy stays slim and links out to the gitea README for full depth (build args, multi-user setup, AWS Bedrock walkthrough, MemPalace deep-dive, language-specific dev sections). Trade-off: image-variants table and quick-start flow are now coupled to `HUB_TEMPLATE` and need a manual edit when they change — explicit and local rather than spread across rules.
|
|
||||||
- **AGENTS.md:** "Documentation coupling on release" rule updated — README edits no longer require regenerating DOCKER_HUB.md. Release-day checklist tightened.
|
|
||||||
- **README pi section:** "What gets installed" sub-section updated to reflect the actual shipped state. Was stale: claimed 6 pi-extensions (actually 7 — mcp-loader was added in pi-extensions commit 141bf64 / 7eec49b / 37cc49e but never propagated here). Each extension now has a one-line description; mcp-loader gets a paragraph covering its dual-transport (local stdio + remote streamable-HTTP per MCP spec 2025-03-26) capability and the `/mcp` slash command. Clarified that the mempalace bridge is a separate MCP entry point that coexists with mcp-loader rather than being replaced by it. Added an explicit note that no MCP servers are baked in beyond mempalace — the loader is opt-in via settings.json edits.
|
- **README pi section:** "What gets installed" sub-section updated to reflect the actual shipped state. Was stale: claimed 6 pi-extensions (actually 7 — mcp-loader was added in pi-extensions commit 141bf64 / 7eec49b / 37cc49e but never propagated here). Each extension now has a one-line description; mcp-loader gets a paragraph covering its dual-transport (local stdio + remote streamable-HTTP per MCP spec 2025-03-26) capability and the `/mcp` slash command. Clarified that the mempalace bridge is a separate MCP entry point that coexists with mcp-loader rather than being replaced by it. Added an explicit note that no MCP servers are baked in beyond mempalace — the loader is opt-in via settings.json edits.
|
||||||
|
|
||||||
## v1.14.41b — 2026-05-08
|
## v1.14.41b — 2026-05-08
|
||||||
|
|
||||||
**Optional pi as second harness.**
|
**Optional pi as second harness.**
|
||||||
|
|
||||||
- **Feature:** New `INSTALL_PI=true` build arg installs [pi](https://github.com/mariozechner/pi-coding-agent) as an alternative or complementary harness alongside opencode. Both harnesses share the same mempalace install and palace path — wing/diary entries are mutually visible. Adds ~150 MB to the image. Pi version pinned by `PI_VERSION` (default: latest at build time); `pi update` inside the container does not persist across `--rm` containers — image rebuild is the upgrade path, same contract as `OPENCODE_VERSION`.
|
- **Feature:** New `INSTALL_PI=true` build arg installs [pi](https://github.com/earendil-works/pi) as an alternative or complementary harness alongside opencode. Both harnesses share the same mempalace install and palace path — wing/diary entries are mutually visible. Adds ~150 MB to the image. Pi version pinned by `PI_VERSION` (default: latest at build time); `pi update` inside the container does not persist across `--rm` containers — image rebuild is the upgrade path, same contract as `OPENCODE_VERSION`.
|
||||||
- **Feature:** New `INSTALL_OPENCODE=false` build arg builds an image without opencode (e.g. for pi-only use). Default remains `true`. Existing builds and tags are unaffected.
|
- **Feature:** New `INSTALL_OPENCODE=false` build arg builds an image without opencode (e.g. for pi-only use). Default remains `true`. Existing builds and tags are unaffected.
|
||||||
- **Feature:** New `devbox-pi-config` named volume mounted at `~/.pi/` persists pi user state (settings.json, `/ext`-disabled extensions) across container recreate. Mirrors the `devbox-opencode-config` pattern from v1.14.33.
|
- **Feature:** New `devbox-pi-config` named volume mounted at `~/.pi/` persists pi user state (settings.json, `/ext`-disabled extensions) across container recreate. Mirrors the `devbox-opencode-config` pattern from v1.14.33.
|
||||||
- **Feature:** Container clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (keybindings, env loader, settings template) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (6 extensions including ext-toggle, todo, ssh-controlmaster, notify, git-checkpoint, confirm-destructive) into `/opt/` at build time. New `PI_TOOLKIT_REF` and `PI_EXTENSIONS_REF` build args (default `main`) pin git refs. The mempalace pi-bridge `mempalace.ts` is symlinked from the existing `/opt/mempalace-toolkit/` clone.
|
- **Feature:** Container clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (keybindings, env loader, settings template) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (6 extensions including ext-toggle, todo, ssh-controlmaster, notify, git-checkpoint, confirm-destructive) into `/opt/` at build time. New `PI_TOOLKIT_REF` and `PI_EXTENSIONS_REF` build args (default `main`) pin git refs. The mempalace pi-bridge `mempalace.ts` is symlinked from the existing `/opt/mempalace-toolkit/` clone.
|
||||||
|
|||||||
+22
-18
@@ -10,13 +10,28 @@ Designed for teams who want a reproducible coding-agent setup that runs the same
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
|
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
|
||||||
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
|
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
|
||||||
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) |
|
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/earendil-works/pi) as alternative/complementary harness (shares the mempalace install with opencode) |
|
||||||
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
|
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
|
||||||
|
|
||||||
All variants support `linux/amd64` and `linux/arm64`.
|
All variants support `linux/amd64` and `linux/arm64`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
|
||||||
|
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
|
||||||
|
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
|
||||||
|
# Edit .env — set OPENCODE_PROVIDER, the matching API key,
|
||||||
|
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
|
||||||
|
docker compose run --rm devbox
|
||||||
|
```
|
||||||
|
|
||||||
|
This drops you straight into opencode with your project mounted at `/workspace`. Use `bash` as the command (e.g. `docker compose run --rm devbox bash`) to land in a shell first — useful for `aws sso login`, `pi` (on `*-with-pi` variants), or multi-harness workflows.
|
||||||
|
|
||||||
|
**One-shot run, no persistence:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e ANTHROPIC_API_KEY=your-key \
|
-e ANTHROPIC_API_KEY=your-key \
|
||||||
@@ -28,27 +43,12 @@ docker run -it --rm \
|
|||||||
joakimp/opencode-devbox:latest
|
joakimp/opencode-devbox:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Drops you straight into opencode with your project mounted at `/workspace`.
|
Full setup guide — authentication for each provider (Anthropic, OpenAI, Bedrock SSO + static), persistence model, build args, troubleshooting: <https://gitea.jordbo.se/joakimp/opencode-devbox#readme>
|
||||||
|
|
||||||
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -it --rm \
|
|
||||||
-e ANTHROPIC_API_KEY=your-key \
|
|
||||||
-e OPENCODE_PROVIDER=anthropic \
|
|
||||||
-v ~/projects:/workspace \
|
|
||||||
-v ~/.ssh:/home/developer/.ssh:ro \
|
|
||||||
joakimp/opencode-devbox:latest bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
|
|
||||||
|
|
||||||
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
|
|
||||||
|
|
||||||
## What's Inside
|
## What's Inside
|
||||||
|
|
||||||
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
|
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
|
||||||
- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
|
- **[pi](https://github.com/earendil-works/pi)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
|
||||||
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
|
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
|
||||||
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
|
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
|
||||||
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
|
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
|
||||||
@@ -86,6 +86,10 @@ Full persistence reference, including multi-user (`SIGNUM`) isolation and host b
|
|||||||
- **Issues / source / docker-compose templates:** <https://gitea.jordbo.se/joakimp/opencode-devbox>
|
- **Issues / source / docker-compose templates:** <https://gitea.jordbo.se/joakimp/opencode-devbox>
|
||||||
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/AGENTS.md>
|
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/AGENTS.md>
|
||||||
|
|
||||||
|
## Sibling images
|
||||||
|
|
||||||
|
- **[`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox)** — pi-only image built on top of this image's base layer. Smaller (~700 MB) and version-tracks the [pi npm package](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) directly. Use this if you want pi without opencode. Source: <https://gitea.jordbo.se/joakimp/pi-devbox>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. See <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/LICENSE>.
|
MIT. See <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/LICENSE>.
|
||||||
|
|||||||
-475
@@ -1,475 +0,0 @@
|
|||||||
# opencode-devbox — portable AI dev environment
|
|
||||||
# Debian-based container with opencode and configurable dev tools
|
|
||||||
|
|
||||||
ARG DEBIAN_VERSION=trixie-slim
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
|
||||||
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG OPENCODE_VERSION=1.14.41
|
|
||||||
|
|
||||||
LABEL maintainer="joakimp"
|
|
||||||
LABEL description="Portable opencode developer container"
|
|
||||||
LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/opencode-devbox"
|
|
||||||
|
|
||||||
# Avoid interactive prompts during build
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# ── Core system packages ─────────────────────────────────────────────
|
|
||||||
# apt-get upgrade picks up any security/CVE fixes published between
|
|
||||||
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
|
||||||
# and the install in the same layer so we don't bloat image history.
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get upgrade -y --no-install-recommends && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
openssh-client \
|
|
||||||
gnupg \
|
|
||||||
jq \
|
|
||||||
ripgrep \
|
|
||||||
fd-find \
|
|
||||||
tree \
|
|
||||||
less \
|
|
||||||
htop \
|
|
||||||
tmux \
|
|
||||||
make \
|
|
||||||
patch \
|
|
||||||
diffutils \
|
|
||||||
git-crypt \
|
|
||||||
age \
|
|
||||||
file \
|
|
||||||
sudo \
|
|
||||||
locales \
|
|
||||||
procps \
|
|
||||||
unzip \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
rsync \
|
|
||||||
python3-pip \
|
|
||||||
python3-venv \
|
|
||||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
|
|
||||||
#
|
|
||||||
# Version policy for the binaries below:
|
|
||||||
# • Default is `latest` — resolved at build time by following the
|
|
||||||
# /releases/latest redirect on GitHub and reading the tag from the
|
|
||||||
# Location header. This means every tagged image picks up the newest
|
|
||||||
# upstream release, with no risk of running months-old CVE-affected
|
|
||||||
# binaries.
|
|
||||||
# • Explicit pins still work: pass `--build-arg GOSU_VERSION=1.19` etc.
|
|
||||||
# Useful for reproducibility or rolling back a bad upstream release.
|
|
||||||
# • Resolved versions are printed during build and re-checked by the
|
|
||||||
# smoke test (scripts/smoke-test.sh), so drift is visible in CI logs.
|
|
||||||
#
|
|
||||||
# The helper `resolve_latest` reads the redirected tag (e.g. "v0.26.1")
|
|
||||||
# and strips a leading "v" if present, yielding a plain version string.
|
|
||||||
|
|
||||||
# gosu — privilege de-escalation
|
|
||||||
ARG GOSU_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
||||||
V="${GOSU_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing gosu ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
|
|
||||||
chmod +x /usr/local/bin/gosu && \
|
|
||||||
gosu --version
|
|
||||||
|
|
||||||
# fzf — fuzzy finder
|
|
||||||
ARG FZF_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
||||||
V="${FZF_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing fzf ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
|
|
||||||
fzf --version
|
|
||||||
|
|
||||||
# git-lfs — Git Large File Storage
|
|
||||||
ARG GIT_LFS_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
||||||
V="${GIT_LFS_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing git-lfs ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
|
|
||||||
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
|
|
||||||
rm -rf /tmp/git-lfs-${V} && \
|
|
||||||
git lfs install --system && \
|
|
||||||
git-lfs --version
|
|
||||||
|
|
||||||
# neovim — modern text editor
|
|
||||||
ARG NVIM_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${NVIM_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing neovim ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
|
|
||||||
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
|
||||||
nvim --version | head -1
|
|
||||||
|
|
||||||
# bat — syntax-highlighted cat replacement
|
|
||||||
ARG BAT_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${BAT_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing bat ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
||||||
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
|
|
||||||
rm -rf /tmp/bat-v${V}-* && \
|
|
||||||
bat --version
|
|
||||||
|
|
||||||
# eza — modern ls replacement
|
|
||||||
ARG EZA_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${EZA_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing eza ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
|
|
||||||
eza --version | head -1
|
|
||||||
|
|
||||||
# zoxide — smarter cd command
|
|
||||||
ARG ZOXIDE_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${ZOXIDE_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing zoxide ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
|
|
||||||
zoxide --version
|
|
||||||
|
|
||||||
# uv — fast Python package manager (replaces pip, venv, pyenv)
|
|
||||||
# Note: uv releases don't prefix tags with "v" (e.g. tag is "0.11.8").
|
|
||||||
ARG UV_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${UV_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing uv ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
||||||
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
|
|
||||||
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
|
|
||||||
rm -rf /tmp/uv-* && \
|
|
||||||
uv --version
|
|
||||||
|
|
||||||
# ── Optional: MemPalace — local-first AI memory system ───────────────
|
|
||||||
# Provides semantic search over conversation history via 29 MCP tools.
|
|
||||||
# Palace data persists via the devbox-palace named volume.
|
|
||||||
# The embedding model (~300 MB) is downloaded on first use and cached
|
|
||||||
# in the palace directory.
|
|
||||||
#
|
|
||||||
# Installed via `uv tool install` into an isolated venv at
|
|
||||||
# /opt/uv-tools/mempalace/. The `mempalace` CLI goes directly on PATH;
|
|
||||||
# the MCP server is reached via the /usr/local/bin/mempalace-mcp-server
|
|
||||||
# wrapper (rootfs/usr/local/bin/mempalace-mcp-server), since system
|
|
||||||
# python3 cannot import from the isolated venv.
|
|
||||||
#
|
|
||||||
# Disable with --build-arg INSTALL_MEMPALACE=false to shave ~300 MB off
|
|
||||||
# the image (chromadb, torch-adjacent deps).
|
|
||||||
ARG INSTALL_MEMPALACE=true
|
|
||||||
ENV UV_TOOL_DIR=/opt/uv-tools
|
|
||||||
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
|
||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
||||||
mkdir -p /opt/uv-tools && \
|
|
||||||
uv tool install --no-cache mempalace && \
|
|
||||||
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
|
||||||
# Thin wrappers (`mempalace-session`, `mempalace-docs`) that delegate to
|
|
||||||
# the mempalace Python CLI for two common scheduled tasks:
|
|
||||||
# - mempalace-session: mines opencode's SQLite session history into
|
|
||||||
# the palace (wing_conversations). Referenced by contrib/ scheduler
|
|
||||||
# templates (systemd user timer, cron) in the toolkit repo.
|
|
||||||
# - mempalace-docs: mines project docs into a per-project wing.
|
|
||||||
# Repo source of truth: https://gitea.jordbo.se/joakimp/mempalace-toolkit
|
|
||||||
#
|
|
||||||
# Requires INSTALL_MEMPALACE=true (wrappers shell out to `mempalace`).
|
|
||||||
# Disable with --build-arg INSTALL_MEMPALACE_TOOLKIT=false if you don't
|
|
||||||
# use the scheduled-mining workflow.
|
|
||||||
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
|
||||||
ARG MEMPALACE_TOOLKIT_REF=main
|
|
||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
|
||||||
git clone --depth 1 --branch "${MEMPALACE_TOOLKIT_REF}" \
|
|
||||||
https://gitea.jordbo.se/joakimp/mempalace-toolkit.git /opt/mempalace-toolkit && \
|
|
||||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
|
||||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
|
||||||
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
|
|
||||||
mempalace-session --help >/dev/null && \
|
|
||||||
mempalace-docs --help >/dev/null && \
|
|
||||||
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# rustup — Rust toolchain manager
|
|
||||||
# Installs the rustup-init binary only. Users bootstrap Rust with:
|
|
||||||
# rustup-init -y && source ~/.cargo/env
|
|
||||||
# Toolchains persist via devbox-rustup and devbox-cargo volumes.
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
|
|
||||||
chmod +x /usr/local/bin/rustup-init
|
|
||||||
|
|
||||||
# gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com)
|
|
||||||
ARG GITEA_MCP_VERSION=latest
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
||||||
V="${GITEA_MCP_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
||||||
fi && \
|
|
||||||
V="${V#v}" && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing gitea-mcp ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
|
|
||||||
| tar -xz -C /usr/local/bin/ gitea-mcp && \
|
|
||||||
chmod +x /usr/local/bin/gitea-mcp && \
|
|
||||||
gitea-mcp --version
|
|
||||||
|
|
||||||
# Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars)
|
|
||||||
# To add more locales, run: sudo sed -i '/<locale>.UTF-8/s/^# //g' /etc/locale.gen && sudo locale-gen
|
|
||||||
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
|
||||||
ENV LANG=en_US.UTF-8
|
|
||||||
ENV LANGUAGE=en_US:en
|
|
||||||
ENV LC_ALL=en_US.UTF-8
|
|
||||||
ENV EDITOR=nvim
|
|
||||||
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
|
||||||
|
|
||||||
# ── Node.js (required for opencode v1.x install + MCP servers) ──────
|
|
||||||
ARG NODE_VERSION=22
|
|
||||||
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
|
|
||||||
apt-get install -y --no-install-recommends nodejs && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# ── Install opencode via npm ─────────────────────────────────────────
|
|
||||||
# v1.x is distributed as an npm package with platform-specific binaries.
|
|
||||||
# Disable with --build-arg INSTALL_OPENCODE=false to build a slimmer
|
|
||||||
# image without opencode (e.g. when only pi is needed). For a fully
|
|
||||||
# pi-only stripped image (no Bun, no opencode), see the pi-devbox repo.
|
|
||||||
ARG INSTALL_OPENCODE=true
|
|
||||||
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
|
||||||
npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
|
||||||
opencode --version ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Optional: pi coding-agent ────────────────────────────────────────
|
|
||||||
# Installs pi as an alternative/complementary harness. Coexists with
|
|
||||||
# opencode in the same image — both share the mempalace install and
|
|
||||||
# palace path, so wing data is mutually visible to either harness.
|
|
||||||
#
|
|
||||||
# pi-toolkit (keybindings.json + pi-env.zsh + settings.example.json)
|
|
||||||
# and pi-extensions (confirm-destructive, ext-toggle, git-checkpoint,
|
|
||||||
# notify, ssh-controlmaster, todo, …) are cloned into /opt/ at build
|
|
||||||
# time. entrypoint-user.sh runs each repo's install.sh on container
|
|
||||||
# start so symlinks land under ~/.pi/agent/ on the named volume.
|
|
||||||
#
|
|
||||||
# Pi version is pinned by PI_VERSION (default: latest at build time).
|
|
||||||
# The baked pi binary lives at /usr/bin/pi (system npm prefix); the
|
|
||||||
# user-writable NPM_CONFIG_PREFIX (~/.pi/npm-global, set further down)
|
|
||||||
# is only consulted by `pi install npm:<pkg>` and `npm install -g` at
|
|
||||||
# runtime — it does NOT shadow the baked pi unless the user does
|
|
||||||
# `npm install -g @mariozechner/pi-coding-agent` themselves, in which
|
|
||||||
# case the user-installed copy on the volume wins via PATH order. Same
|
|
||||||
# contract as OPENCODE_VERSION otherwise: rebuild the image to upgrade
|
|
||||||
# the baked pi.
|
|
||||||
ARG INSTALL_PI=false
|
|
||||||
ARG PI_VERSION=latest
|
|
||||||
ARG PI_TOOLKIT_REF=main
|
|
||||||
ARG PI_EXTENSIONS_REF=main
|
|
||||||
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
|
||||||
if [ "${PI_VERSION}" = "latest" ]; then \
|
|
||||||
npm install -g @mariozechner/pi-coding-agent ; \
|
|
||||||
else \
|
|
||||||
npm install -g @mariozechner/pi-coding-agent@${PI_VERSION} ; \
|
|
||||||
fi && \
|
|
||||||
pi --version && \
|
|
||||||
git clone --depth 1 --branch "${PI_TOOLKIT_REF}" \
|
|
||||||
https://gitea.jordbo.se/joakimp/pi-toolkit.git /opt/pi-toolkit && \
|
|
||||||
git clone --depth 1 --branch "${PI_EXTENSIONS_REF}" \
|
|
||||||
https://gitea.jordbo.se/joakimp/pi-extensions.git /opt/pi-extensions && \
|
|
||||||
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
|
||||||
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
|
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in \
|
|
||||||
amd64) echo "x86_64" ;; \
|
|
||||||
arm64) echo "aarch64" ;; \
|
|
||||||
*) echo "x86_64" ;; \
|
|
||||||
esac) && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
|
|
||||||
unzip -q /tmp/awscli.zip -d /tmp && \
|
|
||||||
/tmp/aws/install && \
|
|
||||||
rm -rf /tmp/aws /tmp/awscli.zip && \
|
|
||||||
aws --version
|
|
||||||
|
|
||||||
# ── Optional: Go ─────────────────────────────────────────────────────
|
|
||||||
# Latest stable Go is resolved from https://go.dev/dl/?mode=json when
|
|
||||||
# GO_VERSION=latest (default). Pass an explicit version like "1.26.2"
|
|
||||||
# to pin.
|
|
||||||
ARG INSTALL_GO=false
|
|
||||||
ARG GO_VERSION=latest
|
|
||||||
RUN if [ "${INSTALL_GO}" = "true" ]; then \
|
|
||||||
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
||||||
V="${GO_VERSION}" && \
|
|
||||||
if [ "$V" = "latest" ]; then \
|
|
||||||
V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \
|
|
||||||
awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
|
|
||||||
fi && \
|
|
||||||
[ -n "$V" ] && \
|
|
||||||
echo "Installing Go ${V}" && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
|
|
||||||
ln -s /usr/local/go/bin/go /usr/local/bin/go && \
|
|
||||||
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
|
|
||||||
# Installs Bun runtime and the oh-my-opencode-slim npm package.
|
|
||||||
# Runtime activation is controlled by ENABLE_OMOS env var in entrypoint.
|
|
||||||
# Uses the baseline Bun build (SSE4.2 only) for compatibility with older
|
|
||||||
# CPUs that lack AVX2 (e.g. Sandy Bridge on OpenStack).
|
|
||||||
ARG INSTALL_OMOS=false
|
|
||||||
ARG OMOS_VERSION=latest
|
|
||||||
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
|
|
||||||
ARCH=$(uname -m) && \
|
|
||||||
if [ "$ARCH" = "x86_64" ]; then \
|
|
||||||
BUN_ARCH="x64-baseline"; \
|
|
||||||
elif [ "$ARCH" = "aarch64" ]; then \
|
|
||||||
BUN_ARCH="aarch64"; \
|
|
||||||
fi && \
|
|
||||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
|
|
||||||
unzip -o /tmp/bun.zip -d /tmp/bun && \
|
|
||||||
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
|
|
||||||
chmod +x /usr/local/bin/bun && \
|
|
||||||
ln -sf bun /usr/local/bin/bunx && \
|
|
||||||
rm -rf /tmp/bun /tmp/bun.zip && \
|
|
||||||
bun --version && \
|
|
||||||
test -L /usr/local/bin/bunx && \
|
|
||||||
npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Non-root user ────────────────────────────────────────────────────
|
|
||||||
ARG USER_NAME=developer
|
|
||||||
ARG USER_UID=1000
|
|
||||||
ARG USER_GID=1000
|
|
||||||
|
|
||||||
RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
|
|
||||||
useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USER_NAME} && \
|
|
||||||
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
|
|
||||||
|
|
||||||
# Create standard directories
|
|
||||||
#
|
|
||||||
# ~/.pi/agent/extensions/ is created proactively so the named volume
|
|
||||||
# mount has a real owner from the first start. The directory is also
|
|
||||||
# what mempalace-toolkit's install_pi_extension probes to decide
|
|
||||||
# whether to deploy the pi↔mempalace bridge — must exist before that
|
|
||||||
# step runs in entrypoint-user.sh.
|
|
||||||
RUN mkdir -p /workspace \
|
|
||||||
/home/${USER_NAME}/.config/opencode/skills \
|
|
||||||
/home/${USER_NAME}/.pi/agent/extensions \
|
|
||||||
/home/${USER_NAME}/.agents/skills \
|
|
||||||
/home/${USER_NAME}/.local/share/opencode \
|
|
||||||
/home/${USER_NAME}/.cache/bash \
|
|
||||||
/home/${USER_NAME}/.ssh && \
|
|
||||||
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
|
|
||||||
|
|
||||||
# ── Pre-warm chromadb embedding model ──────────────────────────────
|
|
||||||
# Mempalace uses chromadb's ONNXMiniLM_L6_V2 embedding function, which
|
|
||||||
# downloads ~80 MB of all-MiniLM-L6-v2 ONNX weights from chromadb's CDN
|
|
||||||
# on first use. Without pre-warming this happens silently (output is
|
|
||||||
# suppressed by the entrypoint init step) and stalls first container
|
|
||||||
# start by minutes on a slow network. We bake the cache at build time
|
|
||||||
# under the developer user's home so the runtime first-start is fast.
|
|
||||||
#
|
|
||||||
# Cache path comes from chromadb's hardcoded `Path.home() / .cache /
|
|
||||||
# chroma / onnx_models / all-MiniLM-L6-v2`. Run as gosu developer so
|
|
||||||
# Path.home() resolves correctly and ownership is right from the start.
|
|
||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
||||||
gosu ${USER_NAME} /opt/uv-tools/mempalace/bin/python -c "\
|
|
||||||
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; \
|
|
||||||
ef = ONNXMiniLM_L6_V2(); \
|
|
||||||
_ = ef(['warmup']); \
|
|
||||||
print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
|
|
||||||
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── User-writable npm global prefix on the devbox-pi-config volume ──
|
|
||||||
# By default npm's global prefix is /usr (writable only by root) so any
|
|
||||||
# `pi install npm:<pkg>` or `npm install -g <pkg>` invoked by the
|
|
||||||
# developer user would EACCES. Pointing the prefix into ~/.pi places
|
|
||||||
# user-installed packages on the named volume, which means they survive
|
|
||||||
# container recreation AND image rebuilds (complementing pi's auto-
|
|
||||||
# restore from settings.json with one less cold-start step).
|
|
||||||
#
|
|
||||||
# These ENVs land AFTER all build-time `npm install -g` calls
|
|
||||||
# (opencode, pi, oh-my-opencode-slim) so those still install to /usr at
|
|
||||||
# build time. They take effect for every runtime invocation regardless
|
|
||||||
# of shell init: docker compose run/exec, login shells, non-interactive
|
|
||||||
# commands. npm auto-creates the prefix directory on first install.
|
|
||||||
#
|
|
||||||
# Harmless when INSTALL_PI=false (and no named volume mounted at ~/.pi):
|
|
||||||
# the dir just lives on the container's writable layer.
|
|
||||||
ENV NPM_CONFIG_PREFIX=/home/${USER_NAME}/.pi/npm-global
|
|
||||||
ENV PATH="/home/${USER_NAME}/.pi/npm-global/bin:${PATH}"
|
|
||||||
|
|
||||||
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
|
||||||
# Shipped under /etc/skel-devbox/ rather than copied directly to the
|
|
||||||
# user's home. The entrypoint copies them to /home/developer/ only if
|
|
||||||
# the target file does not already exist, so host bind-mounts and
|
|
||||||
# previously-customized files are never overwritten. Users can restore
|
|
||||||
# the baked defaults anytime via:
|
|
||||||
# cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
|
|
||||||
# History itself persists via the devbox-shell-history named volume
|
|
||||||
# mounted at ~/.cache/bash (HISTFILE points there).
|
|
||||||
RUN mkdir -p /etc/skel-devbox
|
|
||||||
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
|
||||||
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
|
||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
|
||||||
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
|
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
|
||||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
|
||||||
/usr/local/lib/opencode-devbox/*.py
|
|
||||||
|
|
||||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
ENTRYPOINT ["entrypoint.sh"]
|
|
||||||
# Default to a login shell. `docker compose run --rm devbox` drops
|
|
||||||
# the user into bash to choose: `aws sso login`, then `opencode`
|
|
||||||
# or `pi`. To launch a harness directly, pass it explicitly:
|
|
||||||
# docker compose run --rm devbox opencode
|
|
||||||
# docker compose run --rm devbox pi
|
|
||||||
# `docker compose exec` bypasses the entrypoint and CMD entirely, so
|
|
||||||
# this default has no effect on attach-style workflows.
|
|
||||||
CMD ["bash", "-l"]
|
|
||||||
@@ -11,6 +11,13 @@
|
|||||||
# changes (rootfs/, entrypoint*.sh). Version bumps to OPENCODE_VERSION,
|
# changes (rootfs/, entrypoint*.sh). Version bumps to OPENCODE_VERSION,
|
||||||
# OMOS_VERSION, PI_VERSION, etc. do NOT trigger a base rebuild.
|
# OMOS_VERSION, PI_VERSION, etc. do NOT trigger a base rebuild.
|
||||||
#
|
#
|
||||||
|
# To force a base rebuild for fresh apt packages without other code
|
||||||
|
# changes, bump the BASE_REBUILD_DATE comment below. The hash is
|
||||||
|
# content-addressed over this file, so any byte change invalidates the
|
||||||
|
# cache. Recommended cadence: once per release for security updates.
|
||||||
|
#
|
||||||
|
# BASE_REBUILD_DATE: 2026-05-14 (v1.14.50b — fresh apt + first promote-base-latest)
|
||||||
|
#
|
||||||
# See the project README's "Build pipeline" section for the rationale.
|
# See the project README's "Build pipeline" section for the rationale.
|
||||||
|
|
||||||
ARG DEBIAN_VERSION=trixie-slim
|
ARG DEBIAN_VERSION=trixie-slim
|
||||||
|
|||||||
+16
-7
@@ -32,7 +32,7 @@ ARG USER_NAME=developer
|
|||||||
|
|
||||||
# ── Install opencode via npm ─────────────────────────────────────────
|
# ── Install opencode via npm ─────────────────────────────────────────
|
||||||
ARG INSTALL_OPENCODE=true
|
ARG INSTALL_OPENCODE=true
|
||||||
ARG OPENCODE_VERSION=1.14.41
|
ARG OPENCODE_VERSION=1.15.4
|
||||||
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
||||||
opencode --version ; \
|
opencode --version ; \
|
||||||
@@ -47,16 +47,25 @@ ARG PI_VERSION=latest
|
|||||||
ARG PI_TOOLKIT_REF=main
|
ARG PI_TOOLKIT_REF=main
|
||||||
ARG PI_EXTENSIONS_REF=main
|
ARG PI_EXTENSIONS_REF=main
|
||||||
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
RUN if [ "${INSTALL_PI}" = "true" ]; then \
|
||||||
|
set -e && \
|
||||||
|
git_clone_retry() { \
|
||||||
|
url="$1"; ref="$2"; dest="$3"; \
|
||||||
|
for i in 1 2 3 4 5; do \
|
||||||
|
if git clone --depth 1 --branch "$ref" "$url" "$dest"; then return 0; fi; \
|
||||||
|
rm -rf "$dest"; \
|
||||||
|
echo "git clone $url failed (attempt $i/5), retrying in $((i*5))s..."; \
|
||||||
|
sleep $((i*5)); \
|
||||||
|
done; \
|
||||||
|
return 1; \
|
||||||
|
} && \
|
||||||
if [ "${PI_VERSION}" = "latest" ]; then \
|
if [ "${PI_VERSION}" = "latest" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g @mariozechner/pi-coding-agent ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
|
||||||
else \
|
else \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g @mariozechner/pi-coding-agent@${PI_VERSION} ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
|
||||||
fi && \
|
fi && \
|
||||||
pi --version && \
|
pi --version && \
|
||||||
git clone --depth 1 --branch "${PI_TOOLKIT_REF}" \
|
git_clone_retry https://gitea.jordbo.se/joakimp/pi-toolkit.git "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
||||||
https://gitea.jordbo.se/joakimp/pi-toolkit.git /opt/pi-toolkit && \
|
git_clone_retry https://gitea.jordbo.se/joakimp/pi-extensions.git "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
||||||
git clone --depth 1 --branch "${PI_EXTENSIONS_REF}" \
|
|
||||||
https://gitea.jordbo.se/joakimp/pi-extensions.git /opt/pi-extensions && \
|
|
||||||
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
||||||
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
|
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -8,8 +8,28 @@ The official `ghcr.io/anomalyco/opencode` image (now archived) was Alpine-based
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
**Just want to run it?** No git clone needed — grab the two template files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
|
||||||
|
|
||||||
|
# Pull docker-compose.yml and the .env template
|
||||||
|
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
|
||||||
|
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
|
||||||
|
|
||||||
|
# Edit .env — at minimum: OPENCODE_PROVIDER, the matching API key,
|
||||||
|
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
|
||||||
|
$EDITOR .env
|
||||||
|
|
||||||
|
# Pull and run
|
||||||
|
docker compose run --rm devbox
|
||||||
|
```
|
||||||
|
|
||||||
|
This pulls `joakimp/opencode-devbox:latest` from Docker Hub, mounts `WORKSPACE_PATH` at `/workspace`, and drops you straight into opencode. Use `bash` instead of (no command) to land in a shell first — useful for `aws sso login`, `pi`, `omos`, etc.
|
||||||
|
|
||||||
|
**Want to hack on the image itself, follow upstream changes, or rebuild from source?** Clone the repo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone
|
|
||||||
git clone ssh://gitea.jordbo.se:2222/joakimp/opencode-devbox.git
|
git clone ssh://gitea.jordbo.se:2222/joakimp/opencode-devbox.git
|
||||||
cd opencode-devbox
|
cd opencode-devbox
|
||||||
|
|
||||||
@@ -17,7 +37,7 @@ cd opencode-devbox
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your provider, API key, workspace path, git config
|
# Edit .env with your provider, API key, workspace path, git config
|
||||||
|
|
||||||
# Install git hooks (secret scanning)
|
# Install git hooks (secret scanning) before committing
|
||||||
brew install gitleaks # macOS / Linuxbrew
|
brew install gitleaks # macOS / Linuxbrew
|
||||||
./setup-hooks.sh
|
./setup-hooks.sh
|
||||||
|
|
||||||
@@ -342,8 +362,8 @@ docker compose build --build-arg NVIM_VERSION=0.12.1 # pin to a specific versi
|
|||||||
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
| `INSTALL_MEMPALACE_TOOLKIT` | `true` | [mempalace-toolkit](https://gitea.jordbo.se/joakimp/mempalace-toolkit) bash wrappers (`mempalace-session`, `mempalace-docs`). Cloned at build time from `MEMPALACE_TOOLKIT_REF` (default `main`). Requires `INSTALL_MEMPALACE=true`. |
|
||||||
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
| `INSTALL_OMOS` | `false` | [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration (installs Bun and plugin) |
|
||||||
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a pi-only image (still includes Bun if `INSTALL_OMOS=true`; for a fully stripped pi-only image see the `pi-devbox` repo). |
|
| `INSTALL_OPENCODE` | `true` | Install opencode. Set `false` to build a pi-only image (still includes Bun if `INSTALL_OMOS=true`; for a fully stripped pi-only image see the `pi-devbox` repo). |
|
||||||
| `INSTALL_PI` | `false` | Install [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness. Both clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (~5 MB) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (~1 MB) into `/opt/`; entrypoint deploys them on container start. ~150 MB total image growth. |
|
| `INSTALL_PI` | `false` | Install [pi](https://github.com/earendil-works/pi) as alternative/complementary harness. Both clones [pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit) (~5 MB) and [pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions) (~1 MB) into `/opt/`; entrypoint deploys them on container start. ~150 MB total image growth. |
|
||||||
| `PI_VERSION` | `latest` | npm version of `@mariozechner/pi-coding-agent`. Floats by default (image rebuild = pi update). |
|
| `PI_VERSION` | `latest` | npm version of `@earendil-works/pi-coding-agent`. Floats by default (image rebuild = pi update). |
|
||||||
| `PI_TOOLKIT_REF`, `PI_EXTENSIONS_REF` | `main` | Git refs for the toolkit/extensions clones. Pin to a tag/commit for reproducibility. |
|
| `PI_TOOLKIT_REF`, `PI_EXTENSIONS_REF` | `main` | Git refs for the toolkit/extensions clones. Pin to a tag/commit for reproducibility. |
|
||||||
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
| `OPENCODE_VERSION` | *(pinned per release)* | opencode npm version. Drives the image tag and is intentionally not floated. |
|
||||||
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
| `NODE_VERSION` | `22` | Node.js major version. Pinned to protect against upstream breaking changes across majors. |
|
||||||
@@ -408,7 +428,7 @@ All six agents should respond if your provider authentication is working.
|
|||||||
|
|
||||||
## pi (alternative/complementary harness)
|
## pi (alternative/complementary harness)
|
||||||
|
|
||||||
[pi](https://github.com/mariozechner/pi-coding-agent) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the mempalace install and palace data — wing/diary entries created by one are visible to the other.
|
[pi](https://github.com/earendil-works/pi) is a lightweight TUI coding-agent that can run alongside opencode in the same container. Both harnesses share the mempalace install and palace data — wing/diary entries created by one are visible to the other.
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@@ -467,7 +487,7 @@ docker compose exec -u developer devbox bash
|
|||||||
- `~/.pi/agent/git/<host>/<path>/` (pi packages installed via `pi install git:...`).
|
- `~/.pi/agent/git/<host>/<path>/` (pi packages installed via `pi install git:...`).
|
||||||
- `~/.pi/npm-global/` (pi packages installed via `pi install npm:...`, plus any `npm install -g` invoked as the `developer` user). `NPM_CONFIG_PREFIX` is pre-set in the image, the prefix's `bin/` is on `PATH`, and the directory itself lives on the volume — so user-installed themes, skills, and extensions survive everything short of `docker compose down -v`.
|
- `~/.pi/npm-global/` (pi packages installed via `pi install npm:...`, plus any `npm install -g` invoked as the `developer` user). `NPM_CONFIG_PREFIX` is pre-set in the image, the prefix's `bin/` is on `PATH`, and the directory itself lives on the volume — so user-installed themes, skills, and extensions survive everything short of `docker compose down -v`.
|
||||||
|
|
||||||
The **baked** pi binary (and pi-toolkit / pi-extensions repos under `/opt/`) live on the image filesystem, not the volume. Image rebuild is the upgrade path for those — same contract as `OPENCODE_VERSION`. If you `npm install -g @mariozechner/pi-coding-agent` yourself, the user-installed copy on the volume wins via `PATH` order and survives image rebuilds.
|
The **baked** pi binary (and pi-toolkit / pi-extensions repos under `/opt/`) live on the image filesystem, not the volume. Image rebuild is the upgrade path for those — same contract as `OPENCODE_VERSION`. If you `npm install -g @earendil-works/pi-coding-agent` yourself, the user-installed copy on the volume wins via `PATH` order and survives image rebuilds.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -64,13 +64,28 @@ Designed for teams who want a reproducible coding-agent setup that runs the same
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
|
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
|
||||||
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
|
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
|
||||||
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/mariozechner/pi-coding-agent) as alternative/complementary harness (shares the mempalace install with opencode) |
|
| `latest-with-pi` / `vX.Y.Z-with-pi` | Base + [pi](https://github.com/earendil-works/pi) as alternative/complementary harness (shares the mempalace install with opencode) |
|
||||||
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
|
| `latest-omos-with-pi` / `vX.Y.Z-omos-with-pi` | OMOS + pi together |
|
||||||
|
|
||||||
All variants support `linux/amd64` and `linux/arm64`.
|
All variants support `linux/amd64` and `linux/arm64`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
|
||||||
|
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
|
||||||
|
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
|
||||||
|
# Edit .env — set OPENCODE_PROVIDER, the matching API key,
|
||||||
|
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
|
||||||
|
docker compose run --rm devbox
|
||||||
|
```
|
||||||
|
|
||||||
|
This drops you straight into opencode with your project mounted at `/workspace`. Use `bash` as the command (e.g. `docker compose run --rm devbox bash`) to land in a shell first — useful for `aws sso login`, `pi` (on `*-with-pi` variants), or multi-harness workflows.
|
||||||
|
|
||||||
|
**One-shot run, no persistence:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm \\
|
docker run -it --rm \\
|
||||||
-e ANTHROPIC_API_KEY=your-key \\
|
-e ANTHROPIC_API_KEY=your-key \\
|
||||||
@@ -82,27 +97,12 @@ docker run -it --rm \\
|
|||||||
joakimp/opencode-devbox:latest
|
joakimp/opencode-devbox:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Drops you straight into opencode with your project mounted at `/workspace`.
|
Full setup guide — authentication for each provider (Anthropic, OpenAI, Bedrock SSO + static), persistence model, build args, troubleshooting: <{GITEA}#readme>
|
||||||
|
|
||||||
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -it --rm \\
|
|
||||||
-e ANTHROPIC_API_KEY=your-key \\
|
|
||||||
-e OPENCODE_PROVIDER=anthropic \\
|
|
||||||
-v ~/projects:/workspace \\
|
|
||||||
-v ~/.ssh:/home/developer/.ssh:ro \\
|
|
||||||
joakimp/opencode-devbox:latest bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
|
|
||||||
|
|
||||||
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
|
|
||||||
|
|
||||||
## What's Inside
|
## What's Inside
|
||||||
|
|
||||||
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
|
- **[opencode](https://opencode.ai)** — primary coding-agent harness. Multi-provider (Anthropic, OpenAI, Bedrock, Google, Groq, etc.).
|
||||||
- **[pi](https://github.com/mariozechner/pi-coding-agent)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
|
- **[pi](https://github.com/earendil-works/pi)** *(in `*-with-pi` variants)* — lightweight TUI coding-agent that coexists with opencode and shares the same mempalace install. Includes the `mcp-loader` extension so any local-stdio or remote streamable-HTTP MCP server (searxng, gitea, context7, …) can be added by editing `~/.pi/agent/settings.json`.
|
||||||
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
|
- **[mempalace](https://github.com/MemPalace/mempalace)** — persistent AI memory layer (ChromaDB + SQLite). Wing/diary/knowledge-graph entries are mutually visible to opencode and pi.
|
||||||
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
|
- **[oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)** *(in `*-omos` variants)* — multi-agent orchestration on top of opencode (council, fallback chains, named agents).
|
||||||
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
|
- **AWS CLI v2** with SSO support, **Node.js LTS**, **Bun** (OMOS variants), **uv** (Python), **gosu** for clean UID/GID adjustment to match your host workspace.
|
||||||
@@ -140,6 +140,10 @@ Full persistence reference, including multi-user (`SIGNUM`) isolation and host b
|
|||||||
- **Issues / source / docker-compose templates:** <{GITEA}>
|
- **Issues / source / docker-compose templates:** <{GITEA}>
|
||||||
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <{GITEA}/src/branch/main/AGENTS.md>
|
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <{GITEA}/src/branch/main/AGENTS.md>
|
||||||
|
|
||||||
|
## Sibling images
|
||||||
|
|
||||||
|
- **[`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox)** — pi-only image built on top of this image's base layer. Smaller (~700 MB) and version-tracks the [pi npm package](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) directly. Use this if you want pi without opencode. Source: <https://gitea.jordbo.se/joakimp/pi-devbox>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. See <{GITEA}/src/branch/main/LICENSE>.
|
MIT. See <{GITEA}/src/branch/main/LICENSE>.
|
||||||
|
|||||||
+16
-5
@@ -185,8 +185,13 @@ if [ "$VARIANT" = "omos" ] || [ "$VARIANT" = "omos-with-pi" ]; then
|
|||||||
run "bun (omos)" "bun --version"
|
run "bun (omos)" "bun --version"
|
||||||
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
||||||
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
||||||
# verify it shows up in the global module list.
|
# verify it shows up in the global module list. We must explicitly point
|
||||||
run "oh-my-opencode-slim" "npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
# npm at the system prefix (/usr) here: the image's NPM_CONFIG_PREFIX env
|
||||||
|
# is set to /home/developer/.pi/npm-global so user-installed packages
|
||||||
|
# land on the persistent volume — which means a default `npm ls -g`
|
||||||
|
# queries the user prefix and would miss the baked binaries even though
|
||||||
|
# they're correctly on PATH at /usr/bin.
|
||||||
|
run "oh-my-opencode-slim" "NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
||||||
else
|
else
|
||||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
|
||||||
fail "bun should NOT be in base image but was found"
|
fail "bun should NOT be in base image but was found"
|
||||||
@@ -284,14 +289,20 @@ 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 3200 MB, with-pi adds ~150 MB.
|
# Thresholds (uncompressed): base 2500 MB, omos 3300 MB, with-pi adds ~150 MB.
|
||||||
# omos bumped 3000→3200 on v1.14.31c — mempalace-toolkit bake-in pushed the
|
# omos bumped 3000→3200 on v1.14.31c — mempalace-toolkit bake-in pushed the
|
||||||
|
# baseline; bumped 3200→3300 on v1.15.0 — opencode 1.15.0 came in at
|
||||||
|
# 3206 MB, leaving zero headroom for routine apt-get upgrade drift.
|
||||||
|
# omos-with-pi bumped 3400→3500 on v1.15.0 alongside the omos bump.
|
||||||
|
# omos-with-pi bumped 3500→3700 on v1.15.4b — omos+pi compounded as both
|
||||||
|
# upstream packages grew (opencode 1.15.0→1.15.4, pi 0.74.0→0.75.3) and
|
||||||
|
# the variant landed just over 3500 in v1.15.4's smoke.
|
||||||
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
|
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
|
||||||
# guardrail, not a performance limit.
|
# guardrail, not a performance limit.
|
||||||
THRESHOLD=2500
|
THRESHOLD=2500
|
||||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3200
|
[ "$VARIANT" = "omos" ] && THRESHOLD=3300
|
||||||
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2700
|
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2700
|
||||||
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3400
|
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3700
|
||||||
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